From 91d36e99fb07aa68756c4b7f7c1b621b6f3713a1 Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Mon, 6 Jul 2026 22:52:19 +0200 Subject: [PATCH 01/68] =?UTF-8?q?feat:=20pg=5Fautoctl=20node=20=E2=80=94?= =?UTF-8?q?=20declarative=20node=20lifecycle=20from=20pg=5Fautoctl=5Fnode.?= =?UTF-8?q?ini?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a new pg_autoctl node sub-command tree and a NodeSpec file format designed as the recommended entry-point for container and Kubernetes deployments. ## pg_autoctl_node.ini sections [node] kind (postgres|monitor|coordinator|worker), name, hostname, port [postgresql] pgdata [monitor] pguri (empty for monitor nodes), no_monitor, node_id [formation] name, group [settings] candidate_priority, replication_quorum ← mutable, applied live [options] ssl, auth, pg_hba_lan ← create-time only [ssl] ssl_ca_file, ssl_cert_file, ssl_key_file [launch] mode=deferred: wait for pg_autoctl node start [formation N] monitor only: additional named formations ## pg_autoctl node sub-commands run Read ini, create node if absent, exec() into supervisor. Sets PG_AUTOCTL_NODESPEC so the supervisor watches for live [settings] changes via inotify (Linux) or mtime poll. apply Converge mutable settings on an already-running node. start [] Clear launch=deferred so a waiting node run proceeds. show Dump live config as pg_autoctl_node.ini on stdout. check Parse-only validate; print resolved fields. ## Supervisor file watcher (nodespec_watcher) The supervisor initialises a NodeSpecWatcher when PG_AUTOCTL_NODESPEC is set. Every tick it checks for file changes: - Linux: drain inotify IN_CLOSE_WRITE / IN_MOVED_TO events - Others: stat() every NODESPEC_WATCH_INTERVAL_SECS (10 s) On change, re-parse [settings] and call nodespec_apply() to converge mutable fields without restarting the node. ## Files src/bin/pg_autoctl/cli_node.c / cli_node.h src/bin/pg_autoctl/nodespec.c / nodespec.h src/bin/pg_autoctl/supervisor.c / supervisor.h (watcher integration) docs/ref/pg_autoctl_node.rst docs/ref/pg_autoctl_node_run.rst --- docs/index.rst | 6 + docs/ref/manual.rst | 1 + docs/ref/pg_autoctl_node.rst | 241 ++++++++ docs/ref/pg_autoctl_node_run.rst | 68 +++ src/bin/pg_autoctl/cli_node.c | 534 ++++++++++++++++++ src/bin/pg_autoctl/cli_node.h | 16 + src/bin/pg_autoctl/cli_root.c | 2 + src/bin/pg_autoctl/nodespec.c | 942 +++++++++++++++++++++++++++++++ src/bin/pg_autoctl/nodespec.h | 160 ++++++ src/bin/pg_autoctl/supervisor.c | 35 ++ src/bin/pg_autoctl/supervisor.h | 14 +- 11 files changed, 2018 insertions(+), 1 deletion(-) create mode 100644 docs/ref/pg_autoctl_node.rst create mode 100644 docs/ref/pg_autoctl_node_run.rst create mode 100644 src/bin/pg_autoctl/cli_node.c create mode 100644 src/bin/pg_autoctl/cli_node.h create mode 100644 src/bin/pg_autoctl/nodespec.c create mode 100644 src/bin/pg_autoctl/nodespec.h diff --git a/docs/index.rst b/docs/index.rst index 4abe8fc70..111351d99 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -69,6 +69,12 @@ __ https://github.com/hapostgres/pg_auto_failover ref/manual ref/configuration +.. toctree:: + :hidden: + :caption: Container and Kubernetes + + ref/pg_autoctl_node + .. toctree:: :hidden: :caption: Operations diff --git a/docs/ref/manual.rst b/docs/ref/manual.rst index 93638bea5..d71e00d7b 100644 --- a/docs/ref/manual.rst +++ b/docs/ref/manual.rst @@ -20,6 +20,7 @@ have their own manual page. pg_autoctl_get pg_autoctl_set pg_autoctl_perform + pg_autoctl_node pg_autoctl_inspect pg_autoctl_manual pg_autoctl_do diff --git a/docs/ref/pg_autoctl_node.rst b/docs/ref/pg_autoctl_node.rst new file mode 100644 index 000000000..0dac4c919 --- /dev/null +++ b/docs/ref/pg_autoctl_node.rst @@ -0,0 +1,241 @@ +.. _pg_autoctl_node: + +pg_autoctl node +=============== + +pg_autoctl node - Declarative node lifecycle from a single configuration file + +Synopsis +-------- + +``pg_autoctl node`` manages the full lifecycle of a pg_auto_failover node — +creation, startup, and live reconfiguration — driven by a single +``pg_autoctl_node.ini`` file rather than long sequences of flags:: + + pg_autoctl node + run Create (if needed) and run a node described by a pg_autoctl_node.ini file + apply Apply mutable settings from a pg_autoctl_node.ini to a running node + start Start a node waiting in launch=deferred mode (idempotent) + show Dump current node configuration as a pg_autoctl_node.ini file + check Validate a pg_autoctl_node.ini file without creating anything + +.. toctree:: + :maxdepth: 1 + + pg_autoctl_node_run + +Description +----------- + +``pg_autoctl node`` is the recommended entry point for container and +Kubernetes environments. The complete node description lives in one ini file +that can be version-controlled, templated, and mounted into a container. + +A single command starts the node from scratch or resumes an existing one:: + + pg_autoctl node run /etc/pgaf/node.ini + +This makes ``pg_autoctl node run`` a natural ``CMD`` or ``command:`` for a +Docker or Kubernetes workload — the same image and the same entry-point +work for every node type (monitor, primary, standby, Citus coordinator, +Citus worker). Per-node differences live entirely in the mounted ini file. + +The ``pg_autoctl_node.ini`` File +-------------------------------- + +The file uses ``.ini`` sections. A typical data node:: + + [node] + kind = postgres + name = node1 + hostname = node1.internal + port = 5432 + + [postgresql] + pgdata = /var/lib/postgresql/data + + [monitor] + pguri = postgres://autoctl_node@monitor:5432/pg_auto_failover + + [formation] + name = default + + [settings] + candidate_priority = 50 + replication_quorum = true + + [options] + ssl = self-signed + auth = trust + pg_hba_lan = true + +A monitor node omits ``[monitor]`` entirely (or leaves ``pguri`` empty):: + + [node] + kind = monitor + hostname = monitor.internal + port = 5432 + + [postgresql] + pgdata = /var/lib/postgresql/monitor + + [formation default] + kind = pgsql + +Section reference +~~~~~~~~~~~~~~~~~ + +``[node]`` + ``kind`` — one of ``postgres``, ``monitor``, ``coordinator``, ``worker``. + ``name``, ``hostname``, ``port``. + +``[postgresql]`` + ``pgdata`` — path to the Postgres data directory. + +``[monitor]`` + ``pguri`` — connection string to the pg_auto_failover monitor. + ``no_monitor = true`` for :ref:`disabled-monitor` mode. + ``node_id`` — required with ``no_monitor``. + +``[formation]`` + ``name`` — formation name, default ``"default"``. + ``group`` — Citus group id (0 = coordinator). + +``[settings]`` *(mutable — changes take effect without restart)* + ``candidate_priority`` — failover weight 0–100, default 50. + ``replication_quorum`` — sync quorum participant, default ``true``. + +``[options]`` *(create-time only — ignored on restart)* + ``ssl`` — ``self-signed``, ``verify-ca``, ``verify-full``, or ``off``. + ``auth`` — ``trust``, ``md5``, ``scram``, or ``cert``. + ``pg_hba_lan`` — add LAN-range entries to ``pg_hba.conf``. + +``[ssl]`` *(for ``verify-ca`` / ``verify-full`` modes)* + ``ssl_ca_file``, ``ssl_cert_file``, ``ssl_key_file``. + +``[launch]`` *(optional — for ordered startup)* + ``mode = deferred`` — hold the node in a wait loop until + ``pg_autoctl node start`` writes ``mode = immediate``. + Useful in orchestrators that need fine-grained control over + the order in which nodes join the formation. + +``[formation ]`` *(monitor kind only — repeat for each non-default formation)* + ``kind`` — ``pgsql`` (default) or ``citus``. + +Live Reconfiguration +-------------------- + +The supervisor that ``pg_autoctl node run`` exec's into watches the ini file +for changes. When it detects a write (via inotify on Linux, mtime polling +elsewhere) it re-reads the ``[settings]`` section and applies any changes +without restarting the node or interrupting replication. + +Fields that are **mutable** and applied live: + +- ``candidate_priority`` +- ``replication_quorum`` + +Fields that are **immutable** (require a node restart to take effect): +``kind``, ``pgdata``, ``hostname``, ``port``, ``monitor.pguri``, all +``[options]`` and ``[ssl]`` values. + +Changing an immutable field while the node is running is logged as a warning; +the new value will take effect the next time the node is started. + +Docker and Kubernetes Usage +--------------------------- + +The fixed default path ``/etc/pgaf/node.ini`` lets every container image +use the same entry-point:: + + CMD ["pg_autoctl", "node", "run", "/etc/pgaf/node.ini"] + +Per-node configuration is then a bind-mount (Docker) or a ConfigMap +volume (Kubernetes), keeping the image itself fully generic. + +**Docker Compose example:** + +.. code-block:: yaml + + services: + monitor: + image: hapostgres/pg_auto_failover:latest + volumes: + - ./config/monitor.ini:/etc/pgaf/node.ini:ro + command: ["pg_autoctl", "node", "run", "/etc/pgaf/node.ini"] + + node1: + image: hapostgres/pg_auto_failover:latest + volumes: + - node1-data:/var/lib/postgresql/data + - ./config/node1.ini:/etc/pgaf/node.ini:ro + command: ["pg_autoctl", "node", "run", "/etc/pgaf/node.ini"] + depends_on: [monitor] + + node2: + image: hapostgres/pg_auto_failover:latest + volumes: + - node2-data:/var/lib/postgresql/data + - ./config/node2.ini:/etc/pgaf/node.ini:ro + command: ["pg_autoctl", "node", "run", "/etc/pgaf/node.ini"] + depends_on: [monitor] + + volumes: + node1-data: + node2-data: + +To promote ``node2`` to a higher failover priority without restarting it, +edit ``node2.ini`` and change ``candidate_priority = 80``, then write the +file. The supervisor picks up the change within seconds. + +**Kubernetes StatefulSet example:** + +.. code-block:: yaml + + apiVersion: apps/v1 + kind: StatefulSet + metadata: + name: pg-node + spec: + replicas: 2 + template: + spec: + containers: + - name: pg-autoctl + image: hapostgres/pg_auto_failover:latest + command: ["pg_autoctl", "node", "run", "/etc/pgaf/node.ini"] + volumeMounts: + - name: node-spec + mountPath: /etc/pgaf + volumes: + - name: node-spec + configMap: + name: pg-autoctl-node-spec + +Updating the ConfigMap triggers the supervisor's file watcher and mutable +settings converge automatically; immutable changes require a pod restart. + +Relationship to ``pg_autoctl create`` and ``pg_autoctl run`` +------------------------------------------------------------ + +``pg_autoctl node run`` is a thin layer on top of the existing machinery — +it translates the ini file into the same flags and exec's into the same +supervisor that ``pg_autoctl create ... --run`` or ``pg_autoctl run`` would +start. There is no hidden API: every behaviour described here maps directly +to documented ``pg_autoctl`` operations. + +You can always switch between approaches: + +- Use ``pg_autoctl node show --pgdata `` to generate an ini file from + an existing node that was created with ``pg_autoctl create``. +- Use ``pg_autoctl create`` and ``pg_autoctl run`` directly when you prefer + explicit flag-based management. + +Both approaches share the same state files, configuration, and monitor +protocol — only the entry point differs. + +See Also +-------- + +:ref:`pg_autoctl_node_run`, :ref:`pg_autoctl_create_postgres`, +:ref:`pg_autoctl_run`, :ref:`pg_autoctl_set` diff --git a/docs/ref/pg_autoctl_node_run.rst b/docs/ref/pg_autoctl_node_run.rst new file mode 100644 index 000000000..51140694c --- /dev/null +++ b/docs/ref/pg_autoctl_node_run.rst @@ -0,0 +1,68 @@ +.. _pg_autoctl_node_run: + +pg_autoctl node run +=================== + +pg_autoctl node run - Create (if needed) and run a node from a pg_autoctl_node.ini file + +Synopsis +-------- + +:: + + pg_autoctl node run [] + + path to the pg_autoctl_node.ini file + (default: /etc/pgaf/node.ini) + +Description +----------- + +``pg_autoctl node run`` is the single entry-point for container-based +deployments. Given a ``pg_autoctl_node.ini`` file it: + +1. Reads and validates the ini file. +2. If ``[launch] mode = deferred``, polls the file until the section is + removed or changed to ``mode = immediate`` (see ``pg_autoctl node start``). +3. Checks whether the node already exists (looks for ``pg_autoctl.cfg`` + inside ``pgdata``). + + - **First start** — builds the ``pg_autoctl create [flags] --run`` + argument list from the ini file and exec's into it, which creates Postgres + and starts the supervisor in one step. + - **Subsequent starts** — applies any mutable setting changes found in the + ini file, then exec's into ``pg_autoctl run --pgdata ``. + +4. Sets the ``PG_AUTOCTL_NODESPEC`` environment variable to the ini file + path before exec'ing, so the supervisor can watch the file for live + changes to ``[settings]``. + +Because the command uses ``execv()``, the pg_autoctl supervisor becomes +the direct child process (PID 1 in a container), preserving the standard +Unix signal contract — ``SIGTERM`` stops the supervisor cleanly, +``SIGHUP`` reloads configuration. + +The ``launch = deferred`` pattern +---------------------------------- + +The ``[launch]`` section enables ordered startup without an external +orchestrator:: + + [launch] + mode = deferred + +A node with ``mode = deferred`` starts the polling loop and waits. A +second container, sidecar, or init script calls:: + + pg_autoctl node start /etc/pgaf/node.ini + +which rewrites the file with ``mode = immediate``. The waiting node detects +the change and proceeds. This is useful when you need to ensure the monitor +is fully up before any data node attempts registration, or when bringing up +Citus workers in a specific order. + +See Also +-------- + +:ref:`pg_autoctl_node`, :ref:`pg_autoctl_create_postgres`, +:ref:`pg_autoctl_run` diff --git a/src/bin/pg_autoctl/cli_node.c b/src/bin/pg_autoctl/cli_node.c new file mode 100644 index 000000000..639480743 --- /dev/null +++ b/src/bin/pg_autoctl/cli_node.c @@ -0,0 +1,534 @@ +/* + * src/bin/pg_autoctl/cli_node.c + * pg_autoctl node — declarative node lifecycle driven by a pg_autoctl_node.ini file. + * + * pg_autoctl node run Create (if needed) then run the supervisor. + * pg_autoctl node apply Converge mutable fields on a running node. + * pg_autoctl node show Dump current config as pg_autoctl_node.ini. + * pg_autoctl node check Validate the file without creating anything. + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the PostgreSQL License. + */ + +#include +#include +#include +#include +#include + +#include "cli_common.h" +#include "cli_node.h" +#include "cli_root.h" +#include "commandline.h" +#include "defaults.h" +#include "env_utils.h" +#include "file_utils.h" +#include "keeper_config.h" +#include "log.h" +#include "nodespec.h" +#include "pgsetup.h" +#include "runprogram.h" +#include "string_utils.h" + + +static int cli_node_run_getopts(int argc, char **argv); +static void cli_node_run(int argc, char **argv); +static int cli_node_apply_getopts(int argc, char **argv); +static void cli_node_apply(int argc, char **argv); +static int cli_node_start_getopts(int argc, char **argv); +static void cli_node_start(int argc, char **argv); +static int cli_node_show_getopts(int argc, char **argv); +static void cli_node_show(int argc, char **argv); +static int cli_node_check_getopts(int argc, char **argv); +static void cli_node_check(int argc, char **argv); + + +static CommandLine node_run_command = + make_command( + "run", + "Create (if needed) and run a node described by a pg_autoctl_node.ini file", + "", + " path to the pg_autoctl_node.ini file\n" + " (default: " PG_AUTOCTL_NODESPEC_PATH ")\n", + cli_node_run_getopts, + cli_node_run); + +static CommandLine node_apply_command = + make_command( + "apply", + "Apply mutable settings from a pg_autoctl_node.ini to a running node", + "", + " path to the pg_autoctl_node.ini file\n", + cli_node_apply_getopts, + cli_node_apply); + +static CommandLine node_start_command = + make_command( + "start", + "Start a node waiting in launch=deferred mode (idempotent)", + "[]", + " path to the pg_autoctl_node.ini file\n" + " (default: " PG_AUTOCTL_NODESPEC_PATH ")\n", + cli_node_start_getopts, + cli_node_start); + +static CommandLine node_show_command = + make_command( + "show", + "Dump current node configuration as a pg_autoctl_node.ini file", + "[--pgdata ]", + " --pgdata location of the Postgres data directory\n", + cli_node_show_getopts, + cli_node_show); + +static CommandLine node_check_command = + make_command( + "check", + "Validate a pg_autoctl_node.ini file without creating anything", + "", + " path to the pg_autoctl_node.ini file\n", + cli_node_check_getopts, + cli_node_check); + +static CommandLine *node_subcommands[] = { + &node_run_command, + &node_apply_command, + &node_start_command, + &node_show_command, + &node_check_command, + NULL +}; + +CommandLine node_commands = + make_command_set( + "node", + "Declarative node lifecycle — create, run, and reconfigure from a .ini file", + NULL, NULL, NULL, node_subcommands); + + +/* ----------------------------------------------------------------------- + * Shared state + * ----------------------------------------------------------------------- */ + +static char nodeSpecPath[MAXPGPATH] = { 0 }; + + +/* ----------------------------------------------------------------------- + * pg_autoctl node run + * ----------------------------------------------------------------------- */ +static int +cli_node_run_getopts(int argc, char **argv) +{ + /* argv[0] is the subcommand name ("run"/"apply"/"check"); the optional + * file path is the first remaining positional argument at argv[1]. */ + if (argc > 1 && argv[1][0] != '-') + { + strlcpy(nodeSpecPath, argv[1], sizeof(nodeSpecPath)); + } + else + { + strlcpy(nodeSpecPath, PG_AUTOCTL_NODESPEC_PATH, sizeof(nodeSpecPath)); + } + + return 0; +} + + +/* + * cli_node_run reads the pg_autoctl_node.ini file, runs `pg_autoctl create + * --run` if PGDATA does not yet exist, or `pg_autoctl run` if it does. + * + * We exec() rather than call the internal functions directly so that: + * 1. The supervisor's fork+exec live-upgrade pattern is preserved. + * 2. The already-running supervisor (PID 1) can restart this process after + * a binary upgrade without special-casing the "node run" path. + */ +static void +cli_node_run(int argc, char **argv) +{ + NodeSpec spec = { 0 }; + char *args[40]; + int nargs; + + if (!nodespec_read(nodeSpecPath, &spec)) + { + exit(EXIT_CODE_BAD_CONFIG); + } + + /* + * [launch] mode = deferred: spin here re-reading nodeSpecPath until + * pg_autoctl node start rewrites it with mode = immediate (or removes + * the [launch] section). The file is the rendezvous point — no external + * sentinel needed. + */ + if (spec.launchDeferred) + { + log_info("Node configured with launch = deferred in \"%s\"; " + "waiting for pg_autoctl node start", nodeSpecPath); + + for (;;) + { + pg_usleep(500 * 1000); /* 0.5 s poll */ + + NodeSpec polled = { 0 }; + if (nodespec_read(nodeSpecPath, &polled) && !polled.launchDeferred) + { + spec = polled; + break; + } + } + + log_info("launch = immediate; proceeding with node initialization"); + } + + /* + * If PGDATA already has a pg_autoctl.cfg, the node was created before. + * In that case run `pg_autoctl run` (no --run, no create flags). + * Otherwise run `pg_autoctl create ... --run`. + */ + char cfgPath[MAXPGPATH]; + bool cfgExists = false; + + if (!IS_EMPTY_STRING_BUFFER(spec.pgdata)) + { + sformat(cfgPath, sizeof(cfgPath), + "%s/pg_autoctl.cfg", spec.pgdata); + cfgExists = file_exists(cfgPath); + } + + if (cfgExists) + { + /* + * Node was already created. Apply any mutable changes that might + * have been made to the spec file since the last run, then hand off + * to the normal run path. + */ + NodeSpec prev = { 0 }; + + /* best-effort: ignore errors if we can't re-read the spec */ + (void) nodespec_read(nodeSpecPath, &prev); + (void) nodespec_apply(&spec, &prev); + + args[0] = (char *) pg_autoctl_program; + args[1] = "run"; + args[2] = "--pgdata"; + args[3] = spec.pgdata; + args[4] = NULL; + nargs = 4; + } + else + { + nargs = nodespec_create_argv(&spec, pg_autoctl_program, + args, 32); + if (nargs < 0) + { + exit(EXIT_CODE_INTERNAL_ERROR); + } + } + + /* Tell the supervisor which spec file to watch for live changes */ + setenv("PG_AUTOCTL_NODESPEC", nodeSpecPath, 1); + + /* + * PG_AUTOCTL_TEST_DELAY: stagger node registration so that node IDs + * are assigned in name order (node1 → id 1, node2 → id 2, …). + * Extract the trailing integer from the node name and sleep 2×N seconds. + */ + if (env_exists("PG_AUTOCTL_TEST_DELAY") && spec.name[0] != '\0') + { + const char *p = spec.name + strlen(spec.name); + while (p > spec.name && isdigit((unsigned char) p[-1])) + { + p--; + } + if (*p != '\0') + { + int n = atoi(p) /* IGNORE-BANNED */; + int secs = 2 * n; + log_info("PG_AUTOCTL_TEST_DELAY: sleeping %ds before " + "registration (node %s, index %d)", + secs, spec.name, n); + sleep(secs); + } + } + + /* Log the command we're about to exec, masking password arguments. */ + { + PQExpBuffer cmd = createPQExpBuffer(); + static const char *pwFlags[] = { + "--monitor-password", + "--replication-password", + "--autoctl-node-password", + NULL + }; + for (int i = 0; i < nargs; i++) + { + if (i > 0) + { + appendPQExpBufferChar(cmd, ' '); + } + + /* Check if the previous arg was a password flag. */ + bool maskThis = false; + if (i > 0) + { + for (int k = 0; pwFlags[k]; k++) + { + if (strcmp(args[i - 1], pwFlags[k]) == 0) + { + maskThis = true; + break; + } + } + } + appendPQExpBufferStr(cmd, maskThis ? "****" : args[i]); + } + log_info("pg_autoctl node run: %s", cmd->data); + destroyPQExpBuffer(cmd); + } + + execv(args[0], args); + + /* If we get here execv failed */ + log_fatal("execv(\"%s\"): %m", args[0]); + exit(EXIT_CODE_INTERNAL_ERROR); +} + + +/* ----------------------------------------------------------------------- + * pg_autoctl node apply + * ----------------------------------------------------------------------- */ +static int +cli_node_apply_getopts(int argc, char **argv) +{ + if (argc > 0 && argv[0][0] != '-') + { + strlcpy(nodeSpecPath, argv[0], sizeof(nodeSpecPath)); + } + else + { + log_error("pg_autoctl node apply requires a file argument"); + exit(EXIT_CODE_BAD_ARGS); + } + return 0; +} + + +static void +cli_node_apply(int argc, char **argv) +{ + NodeSpec new_spec = { 0 }; + NodeSpec cur_spec = { 0 }; + + if (!nodespec_read(nodeSpecPath, &new_spec)) + { + exit(EXIT_CODE_BAD_CONFIG); + } + + /* Read the current spec from the same path as a baseline */ + (void) nodespec_read(nodeSpecPath, &cur_spec); + + if (!nodespec_apply(&new_spec, &cur_spec)) + { + log_error("Failed to apply node spec from \"%s\"", nodeSpecPath); + exit(EXIT_CODE_INTERNAL_ERROR); + } +} + + +/* ----------------------------------------------------------------------- + * pg_autoctl node start [] + * + * Clears launch = deferred in the spec file so a waiting pg_autoctl node run + * proceeds. Idempotent: if the node is already immediate, exits 0 quietly. + * ----------------------------------------------------------------------- */ +static int +cli_node_start_getopts(int argc, char **argv) +{ + if (argc > 1 && argv[1][0] != '-') + { + strlcpy(nodeSpecPath, argv[1], sizeof(nodeSpecPath)); + } + else + { + strlcpy(nodeSpecPath, PG_AUTOCTL_NODESPEC_PATH, sizeof(nodeSpecPath)); + } + + return 0; +} + + +static void +cli_node_start(int argc, char **argv) +{ + NodeSpec spec = { 0 }; + + if (!nodespec_read(nodeSpecPath, &spec)) + { + exit(EXIT_CODE_BAD_CONFIG); + } + + if (!spec.launchDeferred) + { + log_info("Node \"%s\" launch is already immediate; nothing to do", + nodeSpecPath); + exit(0); + } + + spec.launchDeferred = false; + + if (!nodespec_write_to_path(&spec, nodeSpecPath)) + { + log_error("Failed to update \"%s\"", nodeSpecPath); + exit(EXIT_CODE_INTERNAL_ERROR); + } + + log_info("Cleared launch = deferred in \"%s\"; node will now start", + nodeSpecPath); +} + + +/* ----------------------------------------------------------------------- + * pg_autoctl node show [--pgdata ] + * ----------------------------------------------------------------------- */ +static int +cli_node_show_getopts(int argc, char **argv) +{ + /* honour --pgdata from the global keeperOptions */ + return cli_getopt_pgdata(argc, argv); +} + + +static void +cli_node_show(int argc, char **argv) +{ + /* + * Build a NodeSpec from the running keeper/monitor config and emit it. + * We read the existing pg_autoctl.cfg rather than the node spec file so + * that `pg_autoctl node show` reflects the live configuration. + */ + KeeperConfig config = keeperOptions; + + bool missingPgdataIsOk = true; + bool pgIsNotRunningIsOk = true; + bool monitorDisabledIsOk = true; + + if (!keeper_config_read_file(&config, + missingPgdataIsOk, + pgIsNotRunningIsOk, + monitorDisabledIsOk)) + { + log_error("Failed to read pg_autoctl configuration"); + exit(EXIT_CODE_BAD_CONFIG); + } + + NodeSpec spec = { 0 }; + + spec.kind = config.pgSetup.pgKind; + strlcpy(spec.pgdata, config.pgSetup.pgdata, sizeof(spec.pgdata)); + strlcpy(spec.hostname, config.hostname, sizeof(spec.hostname)); + spec.port = config.pgSetup.pgport; + strlcpy(spec.monitor_pguri, config.monitor_pguri, + sizeof(spec.monitor_pguri)); + strlcpy(spec.formation, config.formation, sizeof(spec.formation)); + spec.group = config.groupId; + + /* Mutable settings defaults — we'd need a monitor round-trip for live values */ + spec.candidate_priority = 50; + spec.replication_quorum = true; + + /* Create-time defaults */ + strlcpy(spec.ssl, "self-signed", sizeof(spec.ssl)); + strlcpy(spec.auth, "trust", sizeof(spec.auth)); + spec.pg_hba_lan = true; + + (void) nodespec_write(&spec, stdout); +} + + +/* ----------------------------------------------------------------------- + * pg_autoctl node check + * ----------------------------------------------------------------------- */ +static int +cli_node_check_getopts(int argc, char **argv) +{ + if (argc > 0 && argv[0][0] != '-') + { + strlcpy(nodeSpecPath, argv[0], sizeof(nodeSpecPath)); + } + else + { + log_error("pg_autoctl node check requires a file argument"); + exit(EXIT_CODE_BAD_ARGS); + } + return 0; +} + + +static void +cli_node_check(int argc, char **argv) +{ + NodeSpec spec = { 0 }; + + if (!nodespec_read(nodeSpecPath, &spec)) + { + log_error("Invalid node spec file \"%s\"", nodeSpecPath); + exit(EXIT_CODE_BAD_CONFIG); + } + + const char *kindStr; + switch (spec.kind) + { + case NODE_KIND_UNKNOWN: + { + kindStr = "monitor"; + break; + } + + case NODE_KIND_STANDALONE: + { + kindStr = "postgres"; + break; + } + + case NODE_KIND_CITUS_COORDINATOR: + { + kindStr = "coordinator"; + break; + } + + case NODE_KIND_CITUS_WORKER: + { + kindStr = "worker"; + break; + } + + default: + { + kindStr = "unknown"; + break; + } + } + + fformat(stdout, "Node spec \"%s\" is valid.\n", nodeSpecPath); + fformat(stdout, " kind : %s\n", kindStr); + fformat(stdout, " pgdata : %s\n", spec.pgdata); + fformat(stdout, " hostname : %s\n", spec.hostname); + fformat(stdout, " port : %d\n", spec.port); + + if (spec.kind != NODE_KIND_UNKNOWN) + { + fformat(stdout, " monitor_pguri : %s\n", spec.monitor_pguri); + fformat(stdout, " formation : %s\n", spec.formation); + fformat(stdout, " group : %d\n", spec.group); + } + + fformat(stdout, " candidate_priority : %d\n", spec.candidate_priority); + fformat(stdout, " replication_quorum : %s\n", + spec.replication_quorum ? "true" : "false"); + fformat(stdout, " ssl : %s\n", spec.ssl); + fformat(stdout, " auth : %s\n", spec.auth); + fformat(stdout, " pg_hba_lan : %s\n", + spec.pg_hba_lan ? "true" : "false"); +} diff --git a/src/bin/pg_autoctl/cli_node.h b/src/bin/pg_autoctl/cli_node.h new file mode 100644 index 000000000..c236eeada --- /dev/null +++ b/src/bin/pg_autoctl/cli_node.h @@ -0,0 +1,16 @@ +/* + * src/bin/pg_autoctl/cli_node.h + * pg_autoctl node — declarative node lifecycle from a .ini file. + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the PostgreSQL License. + */ + +#ifndef CLI_NODE_H +#define CLI_NODE_H + +#include "commandline.h" + +extern CommandLine node_commands; + +#endif /* CLI_NODE_H */ diff --git a/src/bin/pg_autoctl/cli_root.c b/src/bin/pg_autoctl/cli_root.c index dd5029687..3c9221109 100644 --- a/src/bin/pg_autoctl/cli_root.c +++ b/src/bin/pg_autoctl/cli_root.c @@ -12,6 +12,7 @@ #include "cli_do_root.h" #include "cli_inspect.h" #include "cli_manual.h" +#include "cli_node.h" #include "cli_root.h" #include "commandline.h" @@ -102,6 +103,7 @@ CommandLine *root_subcommands[] = { &inspect_commands, &manual_commands, &internal_commands, + &node_commands, &do_commands, &service_run_command, diff --git a/src/bin/pg_autoctl/nodespec.c b/src/bin/pg_autoctl/nodespec.c new file mode 100644 index 000000000..54d142c82 --- /dev/null +++ b/src/bin/pg_autoctl/nodespec.c @@ -0,0 +1,942 @@ +/* + * src/bin/pg_autoctl/nodespec.c + * Parse, write, and converge a pg_autoctl_node.ini file. + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the PostgreSQL License. + */ + +#include +#include +#include +#include + +#ifdef __linux__ +#include +#endif + +#include "nodespec.h" +#include "ini_file.h" +#include "ini.h" +#include "log.h" +#include "pgsetup.h" +#include "string_utils.h" +#include "file_utils.h" /* sformat */ +#include "env_utils.h" +#include "cli_root.h" /* pg_autoctl_program */ +#include "runprogram.h" /* Program, run_program, free_program */ + + +/* + * nodespec_read parses a pg_autoctl_node.ini file. + * + * File format: + * + * [node] + * kind = postgres # postgres | monitor | coordinator | worker + * hostname = node1 + * port = 5432 + * + * [postgresql] + * pgdata = /var/lib/postgres/pgaf + * + * [monitor] + * pguri = postgresql://autoctl_node@monitor/pg_auto_failover + * + * [formation] + * name = default + * group = 0 + * + * [settings] + * candidate_priority = 50 + * replication_quorum = true + * + * [options] + * ssl = self-signed + * auth = trust + * pg_hba_lan = true + */ +bool +nodespec_read(const char *path, NodeSpec *spec) +{ + char kindStr[NAMEDATALEN] = { 0 }; + char replicationQuorumStr[8] = { 0 }; + char pgHbaLanStr[8] = { 0 }; + char launchModeStr[16] = { 0 }; + char noMonitorStr[8] = { 0 }; + char citusRoleStr[NAMEDATALEN] = { 0 }; + int port = 5432; + int group = 0; + int candidatePriority = 50; + + /* + * Provide string buffers for booleans and the kind enum — we parse them + * after read_ini_file() returns so we can give decent error messages. + */ + IniOption opts[] = { + /* [node] */ + make_strbuf_option_default("node", "kind", NULL, true, + sizeof(kindStr), kindStr, + "postgres"), + make_strbuf_option_default("node", "name", NULL, false, + sizeof(spec->name), spec->name, + ""), + make_strbuf_option_default("node", "hostname", NULL, false, + sizeof(spec->hostname), spec->hostname, + ""), + make_int_option_default("node", "port", NULL, false, + &port, 5432), + + /* [postgresql] */ + make_strbuf_option("postgresql", "pgdata", NULL, true, + sizeof(spec->pgdata), spec->pgdata), + + /* [monitor] */ + make_strbuf_option_default("monitor", "pguri", NULL, false, + sizeof(spec->monitor_pguri), + spec->monitor_pguri, ""), + make_strbuf_option_default("monitor", "no_monitor", NULL, false, + sizeof(noMonitorStr), noMonitorStr, "false"), + make_int_option_default("monitor", "node_id", NULL, false, + &spec->nodeId, 0), + + /* [formation] */ + make_strbuf_option_default("formation", "name", NULL, false, + sizeof(spec->formation), spec->formation, + "default"), + make_int_option_default("formation", "group", NULL, false, + &group, 0), + + /* [settings] — mutable */ + make_int_option_default("settings", "candidate_priority", NULL, false, + &candidatePriority, 50), + make_strbuf_option_default("settings", "replication_quorum", NULL, false, + sizeof(replicationQuorumStr), + replicationQuorumStr, "true"), + + /* [options] — immutable, used only at create time */ + make_strbuf_option_default("options", "ssl", NULL, false, + sizeof(spec->ssl), spec->ssl, + "self-signed"), + make_strbuf_option_default("options", "auth", NULL, false, + sizeof(spec->auth), spec->auth, + "trust"), + make_strbuf_option_default("options", "pg_hba_lan", NULL, false, + sizeof(pgHbaLanStr), pgHbaLanStr, + "true"), + + /* [ssl] — certificate paths for verify-ca / verify-full mode */ + make_strbuf_option_default("ssl", "ca_file", NULL, false, + sizeof(spec->ssl_ca_file), + spec->ssl_ca_file, ""), + make_strbuf_option_default("ssl", "cert_file", NULL, false, + sizeof(spec->ssl_cert_file), + spec->ssl_cert_file, ""), + make_strbuf_option_default("ssl", "key_file", NULL, false, + sizeof(spec->ssl_key_file), + spec->ssl_key_file, ""), + + /* [launch] — optional section; mode=deferred delays node init */ + make_strbuf_option_default("launch", "mode", NULL, false, + sizeof(launchModeStr), launchModeStr, + "immediate"), + + /* [pg_auto_failover] — monitor: password for autoctl_node role */ + make_strbuf_option_default("pg_auto_failover", "autoctl_node_password", + NULL, false, + sizeof(spec->autoctl_node_password), + spec->autoctl_node_password, ""), + + /* [replication] — postgres: password for pgautofailover_replicator */ + make_strbuf_option_default("replication", "password", NULL, false, + sizeof(spec->replication_password), + spec->replication_password, ""), + + /* [pg_auto_failover] — postgres: password for pgautofailover_monitor */ + make_strbuf_option_default("pg_auto_failover", "monitor_password", + NULL, false, + sizeof(spec->monitor_password), + spec->monitor_password, ""), + + /* [citus] — optional; present only for Citus secondary/read-replica nodes */ + make_strbuf_option_default("citus", "role", NULL, false, + sizeof(citusRoleStr), citusRoleStr, ""), + make_strbuf_option_default("citus", "cluster_name", NULL, false, + sizeof(spec->citusClusterName), + spec->citusClusterName, ""), + + INI_OPTION_LAST + }; + + if (!read_ini_file(path, opts)) + { + log_error("Failed to parse node spec file \"%s\"", path); + return false; + } + + /* resolve kind string → enum */ + if (strcmp(kindStr, "monitor") == 0) + { + spec->kind = NODE_KIND_UNKNOWN; /* handled specially: no formation */ + } + else if (strcmp(kindStr, "postgres") == 0) + { + spec->kind = NODE_KIND_STANDALONE; + } + else if (strcmp(kindStr, "coordinator") == 0) + { + spec->kind = NODE_KIND_CITUS_COORDINATOR; + } + else if (strcmp(kindStr, "worker") == 0) + { + spec->kind = NODE_KIND_CITUS_WORKER; + } + else + { + log_error("Unknown node kind \"%s\" in \"%s\"; " + "expected: monitor, postgres, coordinator, worker", + kindStr, path); + return false; + } + + spec->port = port; + spec->group = group; + spec->candidate_priority = candidatePriority; + + /* parse boolean strings */ + spec->replication_quorum = + (strcmp(replicationQuorumStr, "true") == 0 || + strcmp(replicationQuorumStr, "yes") == 0 || + strcmp(replicationQuorumStr, "1") == 0); + + spec->pg_hba_lan = + (strcmp(pgHbaLanStr, "true") == 0 || + strcmp(pgHbaLanStr, "yes") == 0 || + strcmp(pgHbaLanStr, "1") == 0); + + spec->launchDeferred = (strcmp(launchModeStr, "deferred") == 0); + spec->noMonitor = + (strcmp(noMonitorStr, "true") == 0 || + strcmp(noMonitorStr, "yes") == 0 || + strcmp(noMonitorStr, "1") == 0); + + spec->citusSecondary = (strcmp(citusRoleStr, "secondary") == 0); + + /* + * Second pass: enumerate [formation ] sections. + * The standard IniOption machinery can't handle variable-count sections, + * so we open the file again with ini_load directly. + */ + { + char *fileContents = NULL; + long fileSize = 0L; + + if (read_file(path, &fileContents, &fileSize)) + { + ini_t *raw = ini_load(fileContents, NULL); + free(fileContents); + + if (raw) + { + int nsec = ini_section_count(raw); + spec->formationCount = 0; + + for (int si = 0; si < nsec; si++) + { + const char *sname = ini_section_name(raw, si); + if (!sname) + { + continue; + } + if (strncmp(sname, "formation ", 10) != 0) + { + continue; + } + + const char *fname = sname + 10; /* skip "formation " */ + if (fname[0] == '\0') + { + continue; + } + + if (spec->formationCount >= NODESPEC_MAX_FORMATIONS) + { + log_warn("nodespec: too many [formation ] sections " + "in \"%s\"; max %d", path, NODESPEC_MAX_FORMATIONS); + break; + } + + int fi = spec->formationCount++; + strlcpy(spec->formationNames[fi], fname, + sizeof(spec->formationNames[fi])); + + /* optional: kind = ha (default) */ + int ki = ini_find_property(raw, si, "kind", 0); + if (ki != INI_NOT_FOUND) + { + const char *kv = ini_property_value(raw, si, ki); + if (kv && kv[0]) + { + strlcpy(spec->formationKinds[fi], kv, + sizeof(spec->formationKinds[fi])); + } + else + { + strlcpy(spec->formationKinds[fi], "pgsql", + sizeof(spec->formationKinds[fi])); + } + } + else + { + strlcpy(spec->formationKinds[fi], "pgsql", + sizeof(spec->formationKinds[fi])); + } + } + ini_destroy(raw); + } + } + } + + /* validate: non-monitor nodes need a monitor URI unless no_monitor=true */ + if (spec->kind != NODE_KIND_UNKNOWN && + IS_EMPTY_STRING_BUFFER(spec->monitor_pguri) && + !spec->noMonitor) + { + log_error("Node kind \"%s\" requires [monitor] pguri in \"%s\"", + kindStr, path); + return false; + } + + return true; +} + + +/* + * nodespec_write serialises a NodeSpec as a pg_autoctl_node.ini file. + */ +bool +nodespec_write(const NodeSpec *spec, FILE *out) +{ + const char *kindStr; + + switch (spec->kind) + { + case NODE_KIND_UNKNOWN: + { + kindStr = "monitor"; + break; + } + + case NODE_KIND_STANDALONE: + { + kindStr = "postgres"; + break; + } + + case NODE_KIND_CITUS_COORDINATOR: + { + kindStr = "coordinator"; + break; + } + + case NODE_KIND_CITUS_WORKER: + { + kindStr = "worker"; + break; + } + + default: + { + kindStr = "postgres"; + break; + } + } + + fformat(out, + "[node]\n" + "kind = %s\n", + kindStr); + + if (!IS_EMPTY_STRING_BUFFER(spec->name)) + { + fformat(out, "name = %s\n", spec->name); + } + + fformat(out, + "hostname = %s\n" + "port = %d\n" + "\n" + "[postgresql]\n" + "pgdata = %s\n" + "\n", + spec->hostname, + spec->port, + spec->pgdata); + + if (spec->kind != NODE_KIND_UNKNOWN) + { + if (spec->noMonitor) + { + fformat(out, + "[monitor]\n" + "no_monitor = true\n" + "\n"); + } + else + { + fformat(out, + "[monitor]\n" + "pguri = %s\n" + "\n", + spec->monitor_pguri); + } + + fformat(out, + "[formation]\n" + "name = %s\n" + "group = %d\n" + "\n", + spec->formation, + spec->group); + } + + fformat(out, + "[settings]\n" + "candidate_priority = %d\n" + "replication_quorum = %s\n" + "\n" + "[options]\n" + "ssl = %s\n" + "auth = %s\n" + "pg_hba_lan = %s\n", + spec->candidate_priority, + spec->replication_quorum ? "true" : "false", + spec->ssl, + spec->auth, + spec->pg_hba_lan ? "true" : "false"); + + /* only emit [launch] when deferred — omitting the section means immediate */ + if (spec->launchDeferred) + { + fformat(out, "\n[launch]\nmode = deferred\n"); + } + + /* [formation ] sections — monitor kind only */ + for (int fi = 0; fi < spec->formationCount; fi++) + { + fformat(out, "\n[formation %s]\n", spec->formationNames[fi]); + if (spec->formationKinds[fi][0] && + strcmp(spec->formationKinds[fi], "pgsql") != 0) + { + fformat(out, "kind = %s\n", spec->formationKinds[fi]); + } + } + + return true; +} + + +/* + * nodespec_create_argv builds an argv[] array for: + * + * pg_autoctl create --pgdata

--hostname --pgport + * --monitor [--formation ] [--group ] + * [--ssl-self-signed | --no-ssl] + * [--auth ] [--pg-hba-lan] --run + * + * Returns the number of entries written (terminating NULL not counted). + * args[] must have room for at least 40 pointers. + * + * String values are pointers into *spec — the caller must keep spec alive. + */ +int +nodespec_create_argv(const NodeSpec *spec, + const char *pg_autoctl_path, + char **args, int args_size) +{ + int i = 0; + +#define PUSH(v) do { \ + if (i >= args_size - 1) { \ + log_error("nodespec_create_argv: args[] overflow"); \ + return -1; \ + } \ + args[i++] = (char *) (v); \ +} while (0) + + PUSH(pg_autoctl_path); + PUSH("create"); + + switch (spec->kind) + { + case NODE_KIND_UNKNOWN: + { + PUSH("monitor"); + break; + } + + case NODE_KIND_STANDALONE: + { + PUSH("postgres"); + break; + } + + case NODE_KIND_CITUS_COORDINATOR: + { + PUSH("coordinator"); + break; + } + + case NODE_KIND_CITUS_WORKER: + { + PUSH("worker"); + break; + } + + default: + { + PUSH("postgres"); + break; + } + } + + PUSH("--pgdata"); + PUSH(spec->pgdata); + + if (!IS_EMPTY_STRING_BUFFER(spec->name)) + { + PUSH("--name"); + PUSH(spec->name); + } + + if (!IS_EMPTY_STRING_BUFFER(spec->hostname)) + { + PUSH("--hostname"); + PUSH(spec->hostname); + } + + if (spec->port != 5432) + { + /* static buffer — safe because spec is long-lived */ + static char portbuf[16]; + sformat(portbuf, sizeof(portbuf), "%d", spec->port); + PUSH("--pgport"); + PUSH(portbuf); + } + + if (spec->kind != NODE_KIND_UNKNOWN) + { + if (spec->noMonitor) + { + PUSH("--disable-monitor"); + if (spec->nodeId > 0) + { + static char nodeIdBuf[16]; + sformat(nodeIdBuf, sizeof(nodeIdBuf), "%d", spec->nodeId); + PUSH("--node-id"); + PUSH(nodeIdBuf); + } + } + else + { + PUSH("--monitor"); + PUSH(spec->monitor_pguri); + } + + if (!IS_EMPTY_STRING_BUFFER(spec->formation) && + strcmp(spec->formation, "default") != 0) + { + PUSH("--formation"); + PUSH(spec->formation); + } + + if (spec->kind == NODE_KIND_CITUS_WORKER && spec->group > 0) + { + static char groupbuf[16]; + sformat(groupbuf, sizeof(groupbuf), "%d", spec->group); + PUSH("--group"); + PUSH(groupbuf); + } + } + + /* SSL */ + if (strcmp(spec->ssl, "self-signed") == 0) + { + PUSH("--ssl-self-signed"); + } + else if (strcmp(spec->ssl, "off") == 0) + { + PUSH("--no-ssl"); + } + else if (!IS_EMPTY_STRING_BUFFER(spec->ssl_ca_file)) + { + /* verify-ca / verify-full: pass the cert paths explicitly */ + PUSH("--ssl-ca-file"); + PUSH(spec->ssl_ca_file); + PUSH("--server-cert"); + PUSH(spec->ssl_cert_file); + PUSH("--server-key"); + PUSH(spec->ssl_key_file); + PUSH("--ssl-mode"); + PUSH(spec->ssl); + } + + /* auth */ + if (!IS_EMPTY_STRING_BUFFER(spec->auth)) + { + PUSH("--auth"); + PUSH(spec->auth); + } + + if (spec->pg_hba_lan && spec->kind != NODE_KIND_UNKNOWN) + { + PUSH("--pg-hba-lan"); + } + + /* passwords */ + if (spec->kind == NODE_KIND_UNKNOWN && + !IS_EMPTY_STRING_BUFFER(spec->autoctl_node_password)) + { + PUSH("--autoctl-node-password"); + PUSH(spec->autoctl_node_password); + } + + /* non-default formations to create during monitor init */ + if (spec->kind == NODE_KIND_UNKNOWN) + { + for (int fi = 0; fi < spec->formationCount; fi++) + { + PUSH("--formation"); + PUSH(spec->formationNames[fi]); + } + } + + if (spec->kind != NODE_KIND_UNKNOWN) + { + if (!IS_EMPTY_STRING_BUFFER(spec->monitor_password)) + { + PUSH("--monitor-password"); + PUSH(spec->monitor_password); + } + if (!IS_EMPTY_STRING_BUFFER(spec->replication_password)) + { + PUSH("--replication-password"); + PUSH(spec->replication_password); + } + + /* candidate priority and replication quorum (non-default values) */ + if (spec->candidate_priority != 50) + { + static char pribuf[16]; + sformat(pribuf, sizeof(pribuf), "%d", spec->candidate_priority); + PUSH("--candidate-priority"); + PUSH(pribuf); + } + if (!spec->replication_quorum) + { + PUSH("--replication-quorum"); + PUSH("false"); + } + + /* Citus secondary/read-replica cluster settings */ + if (spec->citusSecondary) + { + PUSH("--citus-secondary"); + } + if (!IS_EMPTY_STRING_BUFFER(spec->citusClusterName)) + { + PUSH("--citus-cluster"); + PUSH(spec->citusClusterName); + } + } + + PUSH("--run"); + + args[i] = NULL; + +#undef PUSH + return i; +} + + +/* + * nodespec_write_to_path writes the spec to the given filesystem path, + * replacing the file in-place. Used by pg_autoctl node start to clear the + * [launch] deferred flag so the waiting pg_autoctl node run can proceed. + */ +bool +nodespec_write_to_path(const NodeSpec *spec, const char *path) +{ + FILE *f = fopen(path, "w"); /* IGNORE-BANNED */ + if (!f) + { + log_error("Cannot open \"%s\" for writing: %m", path); + return false; + } + bool ok = nodespec_write(spec, f); + fclose(f); + return ok; +} + + +/* + * nodespec_apply compares new_spec against old_spec and applies any changes + * to the mutable fields by calling into the keeper / monitor APIs. + * + * Currently mutable fields: + * - candidate_priority → monitor_set_node_candidate_priority() + * - replication_quorum → monitor_set_node_replication_quorum() + * + * The [launch] mode field is handled separately by pg_autoctl node start. + * Applying a spec with mode=deferred to an already-started node is a + * non-fatal warning (ignored). + * + * Immutable fields (kind, pgdata, ssl, auth, pg_hba_lan) are not checked here. + */ +bool +nodespec_apply(const NodeSpec *new_spec, const NodeSpec *old_spec) +{ + bool changed = false; + + if (new_spec->candidate_priority != old_spec->candidate_priority) + { + char prio[16]; + sformat(prio, sizeof(prio), "%d", new_spec->candidate_priority); + + Program prog = run_program(pg_autoctl_program, + "set", "node", "candidate-priority", + "--pgdata", new_spec->pgdata, + prio, NULL); + + if (prog.returnCode != 0) + { + log_warn("nodespec_apply: set candidate-priority %s failed (rc=%d)", + prio, prog.returnCode); + if (prog.stdOut) + { + log_warn("%s", prog.stdOut); + } + } + else + { + log_info("nodespec: applied candidate_priority = %d", + new_spec->candidate_priority); + changed = true; + } + free_program(&prog); + } + + if (new_spec->replication_quorum != old_spec->replication_quorum) + { + const char *quorum = new_spec->replication_quorum ? "true" : "false"; + + Program prog = run_program(pg_autoctl_program, + "set", "node", "replication-quorum", + "--pgdata", new_spec->pgdata, + quorum, NULL); + + if (prog.returnCode != 0) + { + log_warn("nodespec_apply: set replication-quorum %s failed (rc=%d)", + quorum, prog.returnCode); + if (prog.stdOut) + { + log_warn("%s", prog.stdOut); + } + } + else + { + log_info("nodespec: applied replication_quorum = %s", quorum); + changed = true; + } + free_program(&prog); + } + + /* immediate → deferred on an already-started node: non-fatal, ignored */ + if (!old_spec->launchDeferred && new_spec->launchDeferred) + { + log_warn("nodespec_apply: ignoring attempt to set launch=deferred " + "on a node that is already running"); + } + + if (!changed) + { + log_debug("nodespec_apply: no mutable fields changed"); + } + + return true; +} + + +/* ----------------------------------------------------------------------- + * Watcher + * ----------------------------------------------------------------------- */ + +/* + * nodespec_watcher_init sets up file watching for *path. + * On Linux we try inotify first; everywhere else (and on failure) we fall + * back to mtime polling every NODESPEC_WATCH_INTERVAL_SECS. + */ +bool +nodespec_watcher_init(NodeSpecWatcher *w, const char *path) +{ + struct stat st; + + memset(w, 0, sizeof(*w)); + strlcpy(w->path, path, sizeof(w->path)); + + w->last_checked = time(NULL); + +#ifdef __linux__ + w->inotify_fd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC); + + if (w->inotify_fd >= 0) + { + w->watch_fd = inotify_add_watch(w->inotify_fd, path, + IN_CLOSE_WRITE | IN_MOVED_TO); + if (w->watch_fd < 0) + { + log_debug("nodespec_watcher_init: inotify_add_watch(\"%s\"): %m; " + "falling back to mtime poll", path); + close(w->inotify_fd); + w->inotify_fd = -1; + } + else + { + log_info("nodespec: watching \"%s\" via inotify", path); + } + } + else + { + log_debug("nodespec_watcher_init: inotify_init1: %m; " + "falling back to mtime poll"); + w->inotify_fd = -1; + w->watch_fd = -1; + } +#endif + + /* record initial mtime so we don't fire on the very first check */ + if (stat(path, &st) == 0) + { + w->last_mtime = st.st_mtime; + } + else + { + w->last_mtime = 0; + } + + w->active = true; + return true; +} + + +/* + * nodespec_watcher_check is called from the supervisor's 100 ms tick. + * + * Returns true if the file changed and nodespec_apply() was attempted. + */ +bool +nodespec_watcher_check(NodeSpecWatcher *w, const NodeSpec *current) +{ + bool file_changed = false; + + if (!w->active) + { + return false; + } + +#ifdef __linux__ + if (w->inotify_fd >= 0) + { + /* + * Drain all pending inotify events. We don't care about the event + * details — any event means the file was written. + */ + char buf[sizeof(struct inotify_event) + NAME_MAX + 1]; + ssize_t n; + + while ((n = read(w->inotify_fd, buf, sizeof(buf))) > 0) + { + file_changed = true; + } + + if (n < 0 && errno != EAGAIN && errno != EWOULDBLOCK) + { + log_warn("nodespec watcher: inotify read error: %m; " + "switching to mtime poll"); + close(w->inotify_fd); + w->inotify_fd = -1; + w->watch_fd = -1; + } + } + else +#endif + { + /* mtime poll — only stat every NODESPEC_WATCH_INTERVAL_SECS */ + time_t now = time(NULL); + + if (now - w->last_checked < NODESPEC_WATCH_INTERVAL_SECS) + { + return false; + } + + w->last_checked = now; + + struct stat st; + if (stat(w->path, &st) == 0 && st.st_mtime != w->last_mtime) + { + w->last_mtime = st.st_mtime; + file_changed = true; + } + } + + if (!file_changed) + { + return false; + } + + /* File changed — re-parse and apply */ + log_info("nodespec: \"%s\" changed, re-reading and converging", w->path); + + NodeSpec new_spec = { 0 }; + if (!nodespec_read(w->path, &new_spec)) + { + log_warn("nodespec: failed to parse updated file \"%s\"; " + "keeping current configuration", w->path); + return false; + } + + /* Warn about immutable field changes rather than silently ignoring them */ + if (new_spec.kind != current->kind) + { + log_warn("nodespec: 'kind' changed in \"%s\" but cannot be applied " + "to a running node — restart required", w->path); + } + + if (strcmp(new_spec.pgdata, current->pgdata) != 0) + { + log_warn("nodespec: 'pgdata' changed in \"%s\" but cannot be applied " + "to a running node — restart required", w->path); + } + + (void) nodespec_apply(&new_spec, current); + return true; +} + + +/* + * nodespec_watcher_close releases inotify resources. + */ +void +nodespec_watcher_close(NodeSpecWatcher *w) +{ +#ifdef __linux__ + if (w->inotify_fd >= 0) + { + close(w->inotify_fd); + w->inotify_fd = -1; + w->watch_fd = -1; + } +#endif + w->active = false; +} diff --git a/src/bin/pg_autoctl/nodespec.h b/src/bin/pg_autoctl/nodespec.h new file mode 100644 index 000000000..3328643f1 --- /dev/null +++ b/src/bin/pg_autoctl/nodespec.h @@ -0,0 +1,160 @@ +/* + * src/bin/pg_autoctl/nodespec.h + * Declarative node description file (.ini) — read, write, and converge. + * + * A NodeSpec is the in-memory representation of a pg_autoctl_node.ini file. + * It covers all parameters needed to create and run one pg_auto_failover node. + * + * Lifecycle: + * 1. nodespec_read() — parse the file into a NodeSpec + * 2. nodespec_create() — run `pg_autoctl create ` if PGDATA absent + * 3. nodespec_run() — hand off to the normal run path + * 4. nodespec_apply() — converge mutable fields on an already-running node + * + * The supervisor calls nodespec_check_and_apply() on a timer so that editing + * the file and saving it is sufficient to reconfigure a running node. + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the PostgreSQL License. + */ + +#ifndef NODESPEC_H +#define NODESPEC_H + +#include +#include +#include + +#include "postgres_fe.h" /* MAXPGPATH, NAMEDATALEN, etc. */ +#include "pgsql.h" /* MAXCONNINFO */ +#include "pgsetup.h" /* PgInstanceKind, NODE_KIND_* */ + +/* + * Fixed location inside every container. The compose generator writes one + * ini file per node and bind-mounts it here. Having a fixed path means the + * Docker image needs only a single CMD: + * + * CMD ["pg_autoctl", "node", "run", PG_AUTOCTL_NODESPEC_PATH] + */ +#define PG_AUTOCTL_NODESPEC_PATH "/etc/pgaf/node.ini" + +/* + * How often the supervisor polls the spec file for changes (seconds). + * inotify/kqueue are used when available; this is the fallback interval. + */ +#define NODESPEC_WATCH_INTERVAL_SECS 10 + +/* ----------------------------------------------------------------------- + * NodeSpec — one-to-one with the [sections] of pg_autoctl_node.ini + * ----------------------------------------------------------------------- */ + +typedef struct NodeSpec +{ + /* [node] */ + PgInstanceKind kind; /* postgres | coordinator | worker | monitor */ + char name[_POSIX_HOST_NAME_MAX]; /* --name; defaults to hostname when empty */ + char hostname[_POSIX_HOST_NAME_MAX]; + int port; /* Postgres port, default 5432 */ + + /* [postgresql] */ + char pgdata[MAXPGPATH]; + + /* [monitor] — empty for kind == monitor */ + char monitor_pguri[MAXCONNINFO]; + bool noMonitor; /* [monitor] no_monitor=true: standalone mode */ + int nodeId; /* [monitor] node_id: required with --disable-monitor */ + + /* [formation] */ + char formation[NAMEDATALEN]; /* default "default" */ + int group; /* Citus group; 0 = coordinator */ + + /* [settings] — mutable; applied on SIGHUP / file change */ + int candidate_priority; /* 0-100, default 50 */ + bool replication_quorum; /* sync quorum participant, default true */ + + /* [options] — immutable; used only at pg_autoctl create time */ + char ssl[32]; /* self-signed | verify-ca | verify-full | off */ + char auth[32]; /* trust | md5 | scram | cert */ + bool pg_hba_lan; /* add --pg-hba-lan flag */ + + /* [ssl] — certificate paths for verify-ca / verify-full mode */ + char ssl_ca_file[MAXPGPATH]; + char ssl_cert_file[MAXPGPATH]; + char ssl_key_file[MAXPGPATH]; + bool launchDeferred; /* [launch] mode=deferred: wait for node start */ + + /* [formation ] — monitor kind: non-default formations to create */ +#define NODESPEC_MAX_FORMATIONS 16 + int formationCount; + char formationNames[NODESPEC_MAX_FORMATIONS][NAMEDATALEN]; + char formationKinds[NODESPEC_MAX_FORMATIONS][NAMEDATALEN]; /* "pgsql" default */ + + /* [pg_auto_failover] — monitor kind: password for autoctl_node role */ + char autoctl_node_password[MAXCONNINFO]; + + /* [replication] — postgres kind: password for pgautofailover_replicator */ + char replication_password[MAXCONNINFO]; + + /* [pg_auto_failover] — postgres kind: password for pgautofailover_monitor */ + char monitor_password[MAXCONNINFO]; + + /* [citus] — Citus secondary/read-replica cluster settings */ + bool citusSecondary; /* role = secondary */ + char citusClusterName[NAMEDATALEN]; /* cluster_name = */ +} NodeSpec; + +/* ----------------------------------------------------------------------- + * File watcher state — embedded in Supervisor + * ----------------------------------------------------------------------- */ + +typedef struct NodeSpecWatcher +{ + bool active; /* true once nodespec_watcher_init() succeeds */ + char path[MAXPGPATH]; /* path of the watched file */ + time_t last_mtime; /* mtime at last check */ + time_t last_checked; /* wall-clock time of last poll */ + +#ifdef __linux__ + int inotify_fd; /* inotify instance fd, -1 if unavailable */ + int watch_fd; /* inotify watch descriptor */ +#endif +} NodeSpecWatcher; + +/* ----------------------------------------------------------------------- + * API + * ----------------------------------------------------------------------- */ + +/* Parse a pg_autoctl_node.ini file into *spec. Returns false on error. */ +bool nodespec_read(const char *path, NodeSpec *spec); + +/* Write *spec as a pg_autoctl_node.ini file (for pg_autoctl node show). */ +bool nodespec_write(const NodeSpec *spec, FILE *out); + +/* Build the argv[] for `pg_autoctl create [flags]`. + * Caller provides args[] with room for at least 40 char* entries. + * Returns the number of entries filled (not counting the trailing NULL). */ +int nodespec_create_argv(const NodeSpec *spec, + const char *pg_autoctl_path, + char **args, int args_size); + +/* Apply mutable fields (candidate_priority, replication_quorum) to a + * running node by calling into the keeper/monitor APIs directly. + * Logs what changed. Returns false only on hard error. */ +bool nodespec_apply(const NodeSpec *new_spec, const NodeSpec *old_spec); +bool nodespec_write_to_path(const NodeSpec *spec, const char *path); + +/* ----------------------------------------------------------------------- + * Watcher — called from supervisor loop + * ----------------------------------------------------------------------- */ + +/* Initialise watcher state. Sets up inotify on Linux if available. */ +bool nodespec_watcher_init(NodeSpecWatcher *w, const char *path); + +/* Called from the supervisor's 100 ms tick. + * Returns true if the file changed and nodespec_apply() was called. */ +bool nodespec_watcher_check(NodeSpecWatcher *w, const NodeSpec *current); + +/* Release inotify fds (called at supervisor shutdown). */ +void nodespec_watcher_close(NodeSpecWatcher *w); + +#endif /* NODESPEC_H */ diff --git a/src/bin/pg_autoctl/supervisor.c b/src/bin/pg_autoctl/supervisor.c index 9e20853d5..c05c19d04 100644 --- a/src/bin/pg_autoctl/supervisor.c +++ b/src/bin/pg_autoctl/supervisor.c @@ -20,6 +20,7 @@ #include "cli_root.h" #include "defaults.h" +#include "nodespec.h" #include "env_utils.h" #include "fsm.h" #include "keeper.h" @@ -76,6 +77,32 @@ supervisor_start(Service services[], int serviceCount, const char *pidfile) /* copy the pidfile over to our supervisor structure */ strlcpy(supervisor.pidfile, pidfile, MAXPGPATH); + /* + * If we were started by `pg_autoctl node run`, the node spec path is + * passed via the PG_AUTOCTL_NODESPEC env var. Set up the file watcher + * so the supervisor can converge mutable settings when the file changes. + */ + { + char specPath[MAXPGPATH] = { 0 }; + + if (env_exists("PG_AUTOCTL_NODESPEC") && + get_env_copy("PG_AUTOCTL_NODESPEC", specPath, sizeof(specPath)) && + !IS_EMPTY_STRING_BUFFER(specPath)) + { + if (nodespec_read(specPath, &supervisor.watchedSpec) && + nodespec_watcher_init(&supervisor.watcher, specPath)) + { + log_info("Supervisor: watching node spec \"%s\"", specPath); + } + else + { + log_warn("Supervisor: failed to initialise node spec watcher " + "for \"%s\"; changes to the file will be ignored", + specPath); + } + } + } + /* * Create our PID file, or quit now if another pg_autoctl instance is * runnning. @@ -219,6 +246,14 @@ supervisor_loop(Supervisor *supervisor) { /* avoid busy looping on waitpid(WNOHANG) */ pg_usleep(100 * 1000); /* 100 ms */ + + /* + * Check if the node spec file has changed and apply mutable + * settings if so. Uses inotify on Linux, mtime poll elsewhere. + * No-op when watcher.active is false (normal run path). + */ + (void) nodespec_watcher_check(&supervisor->watcher, + &supervisor->watchedSpec); } /* ignore errors */ diff --git a/src/bin/pg_autoctl/supervisor.h b/src/bin/pg_autoctl/supervisor.h index 903b587ae..a043f1a11 100644 --- a/src/bin/pg_autoctl/supervisor.h +++ b/src/bin/pg_autoctl/supervisor.h @@ -12,13 +12,15 @@ #include #include +#include "nodespec.h" + /* * pg_autoctl runs sub-processes as "services", and we need to use the same * service names in several places: * * - the main pidfile, * - the per-service name for the pidfile is derived from this, - * - the pg_autoctl do service getpid|restart commands + * - the pg_autoctl manual service getpid|restart commands */ #define SERVICE_NAME_POSTGRES "postgres" #define SERVICE_NAME_KEEPER "node-active" @@ -122,6 +124,16 @@ typedef struct Supervisor bool shutdownSequenceInProgress; int shutdownSignal; int stoppingLoopCounter; + + /* + * Optional node spec watcher. When pg_autoctl is started via + * `pg_autoctl node run `, the supervisor watches the ini file for + * changes and converges mutable settings automatically. + * + * watcher.active is false when not in use (normal create/run path). + */ + NodeSpecWatcher watcher; + NodeSpec watchedSpec; /* last-applied spec — baseline for diff */ } Supervisor; From 8c5f87396b3815e8d44bcab6498ca566ea2f51c8 Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Mon, 6 Jul 2026 23:26:01 +0200 Subject: [PATCH 02/68] docs,feat: refine pg_autoctl node docs and make ssl mutable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - pg_autoctl_node.rst: replace prose property list with a structured table (section/property/type/mutable/default/description), each property on its own row; fix 'disabled-monitor' label → actual ref - nodespec_apply: add ssl apply block — when [options].ssl or any [ssl] cert path changes, call 'pg_autoctl enable ssl' with the appropriate flags (--ssl-self-signed, --no-ssl, or --ssl-mode + cert paths); update nodespec_apply comment to list ssl as mutable - docs/index.rst: remove 'Container and Kubernetes' as a top-level toctree caption; the manual page is still reachable via Manual Pages - docs/operations.rst: add 'Container and Kubernetes Deployments' section describing pg_autoctl node run, live reconfiguration, and the launch=deferred pattern; cross-refs to pg_autoctl_node for details - docs/ref/configuration.rst: add 'Declarative Node Configuration' section covering pg_autoctl_node.ini sections and their relationship to pg_autoctl.cfg; cross-ref to pg_autoctl_node --- docs/index.rst | 6 - docs/operations.rst | 51 +++++ docs/ref/configuration.rst | 47 +++++ docs/ref/pg_autoctl_node.rst | 374 +++++++++++++++++++--------------- src/bin/pg_autoctl/nodespec.c | 78 ++++++- 5 files changed, 385 insertions(+), 171 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 111351d99..4abe8fc70 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -69,12 +69,6 @@ __ https://github.com/hapostgres/pg_auto_failover ref/manual ref/configuration -.. toctree:: - :hidden: - :caption: Container and Kubernetes - - ref/pg_autoctl_node - .. toctree:: :hidden: :caption: Operations diff --git a/docs/operations.rst b/docs/operations.rst index 6149100d9..c304eb128 100644 --- a/docs/operations.rst +++ b/docs/operations.rst @@ -440,3 +440,54 @@ time -- for instance because a firewall rule hasn't yet activated -- it's possible to try ``pg_autoctl create`` again. pg_auto_failover will review its previous progress and repeat idempotent operations (``create database``, ``create extension`` etc), gracefully handling errors. + +Container and Kubernetes Deployments +------------------------------------- + +``pg_autoctl node`` provides a declarative, file-driven entry-point designed +for container and Kubernetes environments. Instead of assembling long +``pg_autoctl create`` flag sequences per node, the complete node description +lives in a single ``pg_autoctl_node.ini`` file: + +.. code-block:: ini + + [node] + kind = postgres + hostname = node1.internal + port = 5432 + + [postgresql] + pgdata = /var/lib/postgresql/data + + [monitor] + pguri = postgres://autoctl_node@monitor:5432/pg_auto_failover + + [settings] + candidate_priority = 50 + replication_quorum = true + + [options] + ssl = self-signed + +A single command then creates the node if absent and starts the supervisor:: + + pg_autoctl node run /etc/pgaf/node.ini + +This makes ``pg_autoctl node run`` a natural ``CMD`` (Docker) or +``command:`` (Kubernetes) for every node type. The same image works for +monitor, primary, standby, coordinator, and worker nodes — per-node +differences live entirely in the bind-mounted ini file. + +**Live reconfiguration** — the supervisor watches the ini file. Editing +``candidate_priority``, ``replication_quorum``, or the ``ssl`` settings and +saving the file is sufficient to converge the running node; no restart is +required. + +**Ordered startup** — add ``[launch] mode = deferred`` to any node that +should wait for an external signal before initialising. Call +``pg_autoctl node start `` from a sidecar or init container to release +it. This replaces external orchestration for the common case where data +nodes must wait until the monitor is ready. + +For the full property reference, mutability table, and Compose / Kubernetes +examples see :ref:`pg_autoctl_node`. diff --git a/docs/ref/configuration.rst b/docs/ref/configuration.rst index 6157c87a7..52d28a090 100644 --- a/docs/ref/configuration.rst +++ b/docs/ref/configuration.rst @@ -231,3 +231,50 @@ The pg_auto_failover keeper tries to restart PostgreSQL (default 3) or up to ``timeout.postgresql_restart_failure_timeout`` (defaults 20s) since it detected that PostgreSQL is not running, whichever comes first. + +.. _nodespec_configuration: + +Declarative Node Configuration (pg_autoctl_node.ini) +----------------------------------------------------- + +When using :ref:`pg_autoctl_node`, the node is described entirely in a +``pg_autoctl_node.ini`` file. This is an alternative to passing flags on +the ``pg_autoctl create`` command line, and it is the recommended approach +for container and Kubernetes deployments. + +The ini file maps every ``pg_autoctl create`` flag to a named key in one of +the following sections: + +``[node]`` + ``kind``, ``name``, ``hostname``, ``port`` + +``[postgresql]`` + ``pgdata`` + +``[monitor]`` + ``pguri``, ``no_monitor``, ``node_id`` + +``[formation]`` + ``name``, ``group`` + +``[settings]`` *(mutable — changes applied live without restart)* + ``candidate_priority``, ``replication_quorum`` + +``[options]`` + ``ssl`` *(mutable — applied via* ``pg_autoctl enable ssl`` *)*, + ``auth`` *(create-time only)*, + ``pg_hba_lan`` *(create-time only)* + +``[ssl]`` *(mutable — changes applied via* ``pg_autoctl enable ssl`` *)* + ``ca_file``, ``cert_file``, ``key_file`` + +The pg_autoctl keeper configuration file (``pg_autoctl.cfg`` inside +``PGDATA``) is still the authoritative runtime configuration. The +``pg_autoctl_node.ini`` file is read at startup and on every file-change +event; its mutable fields are converged into the running node state, while +immutable fields are only applied at node creation time. + +Use ``pg_autoctl node show --pgdata

`` to generate a +``pg_autoctl_node.ini`` from an existing node's current configuration. + +See :ref:`pg_autoctl_node` for the full property reference. diff --git a/docs/ref/pg_autoctl_node.rst b/docs/ref/pg_autoctl_node.rst index 0dac4c919..161e0cee9 100644 --- a/docs/ref/pg_autoctl_node.rst +++ b/docs/ref/pg_autoctl_node.rst @@ -10,7 +10,7 @@ Synopsis ``pg_autoctl node`` manages the full lifecycle of a pg_auto_failover node — creation, startup, and live reconfiguration — driven by a single -``pg_autoctl_node.ini`` file rather than long sequences of flags:: +``pg_autoctl_node.ini`` file rather than long sequences of command-line flags:: pg_autoctl node run Create (if needed) and run a node described by a pg_autoctl_node.ini file @@ -27,23 +27,17 @@ creation, startup, and live reconfiguration — driven by a single Description ----------- -``pg_autoctl node`` is the recommended entry point for container and -Kubernetes environments. The complete node description lives in one ini file -that can be version-controlled, templated, and mounted into a container. - -A single command starts the node from scratch or resumes an existing one:: - - pg_autoctl node run /etc/pgaf/node.ini - -This makes ``pg_autoctl node run`` a natural ``CMD`` or ``command:`` for a -Docker or Kubernetes workload — the same image and the same entry-point -work for every node type (monitor, primary, standby, Citus coordinator, -Citus worker). Per-node differences live entirely in the mounted ini file. +``pg_autoctl node run `` is the recommended entry-point for container +and Kubernetes deployments. The complete node description lives in one ini +file that can be version-controlled, templated, and bind-mounted into a +container. The same image and the same entry-point work for every node type +(monitor, primary, standby, Citus coordinator, Citus worker); per-node +differences live entirely in the mounted ini file. The ``pg_autoctl_node.ini`` File -------------------------------- -The file uses ``.ini`` sections. A typical data node:: +The file uses ``.ini`` sections. A typical data node:: [node] kind = postgres @@ -65,16 +59,16 @@ The file uses ``.ini`` sections. A typical data node:: replication_quorum = true [options] - ssl = self-signed - auth = trust + ssl = self-signed + auth = trust pg_hba_lan = true -A monitor node omits ``[monitor]`` entirely (or leaves ``pguri`` empty):: +A monitor node omits ``[monitor]`` entirely:: [node] - kind = monitor + kind = monitor hostname = monitor.internal - port = 5432 + port = 5432 [postgresql] pgdata = /var/lib/postgresql/monitor @@ -82,157 +76,219 @@ A monitor node omits ``[monitor]`` entirely (or leaves ``pguri`` empty):: [formation default] kind = pgsql -Section reference -~~~~~~~~~~~~~~~~~ +Section and Property Reference +------------------------------- + +The table below lists every property, its section, whether it can be changed +on a running node without a restart, and its default value. + +.. list-table:: + :widths: 18 14 8 12 48 + :header-rows: 1 + + * - Section / Property + - Type + - Mutable + - Default + - Description + * - **[node]** + - + - + - + - + * - ``kind`` + - string + - No + - — + - Node role: ``postgres``, ``monitor``, ``coordinator``, or ``worker`` + * - ``name`` + - string + - No + - hostname + - Human-readable node name shown in ``pg_autoctl show state`` + * - ``hostname`` + - string + - No + - auto-detected + - Address other nodes use to reach this node + * - ``port`` + - integer + - No + - 5432 + - Postgres port number + * - **[postgresql]** + - + - + - + - + * - ``pgdata`` + - path + - No + - — + - Path to the Postgres data directory + * - **[monitor]** + - + - + - + - + * - ``pguri`` + - connstring + - No + - — + - Connection string to the pg_auto_failover monitor. Empty for monitor nodes. + * - ``no_monitor`` + - boolean + - No + - false + - Set ``true`` for :ref:`pg_autoctl_disable_monitor` (standalone) mode + * - ``node_id`` + - integer + - No + - — + - Required with ``no_monitor = true`` + * - **[formation]** + - + - + - + - + * - ``name`` + - string + - No + - default + - Formation name + * - ``group`` + - integer + - No + - 0 + - Citus group id (0 = coordinator) + * - **[settings]** *(applied live without restart)* + - + - + - + - + * - ``candidate_priority`` + - integer 0–100 + - **Yes** + - 50 + - Failover weight. 0 means never promote this node. + * - ``replication_quorum`` + - boolean + - **Yes** + - true + - Whether this node participates in the synchronous replication quorum + * - **[options]** + - + - + - + - + * - ``ssl`` + - string + - **Yes** + - self-signed + - SSL mode: ``self-signed``, ``verify-ca``, ``verify-full``, or ``off``. + Changes call ``pg_autoctl enable ssl`` on the running node. + * - ``auth`` + - string + - No + - trust + - Authentication method written into ``pg_hba.conf`` at create time: + ``trust``, ``md5``, ``scram``, or ``cert`` + * - ``pg_hba_lan`` + - boolean + - No + - true + - Add LAN-range entries to ``pg_hba.conf`` at create time + * - **[ssl]** *(for ``verify-ca`` / ``verify-full`` modes; applied live)* + - + - + - + - + * - ``ca_file`` + - path + - **Yes** + - — + - Path to the CA certificate file (``ssl_ca_file`` in postgresql.conf) + * - ``cert_file`` + - path + - **Yes** + - — + - Path to the server certificate (``ssl_cert_file``) + * - ``key_file`` + - path + - **Yes** + - — + - Path to the server private key (``ssl_key_file``) + * - **[launch]** *(optional)* + - + - + - + - + * - ``mode`` + - string + - — + - immediate + - ``deferred``: hold the node in a wait loop until ``pg_autoctl node start`` + writes ``immediate``. Useful for ordered startup in compose or k8s. + * - **[formation ]** *(monitor kind only, repeat for each extra formation)* + - + - + - + - + * - ``kind`` + - string + - No + - pgsql + - Formation kind: ``pgsql`` or ``citus`` + +Additional sections for passwords (kept out of the main ini where possible): + +``[pg_auto_failover]`` + ``autoctl_node_password`` — password for the ``pgautofailover_monitor`` role + (monitor nodes), or the ``autoctl_node`` role (data nodes). + +``[replication]`` + ``replication_password`` — password for the ``pgautofailover_replicator`` role. + +``[citus]`` + ``role = secondary`` and ``cluster_name`` for Citus secondary clusters. -``[node]`` - ``kind`` — one of ``postgres``, ``monitor``, ``coordinator``, ``worker``. - ``name``, ``hostname``, ``port``. +Live Reconfiguration +-------------------- -``[postgresql]`` - ``pgdata`` — path to the Postgres data directory. +The supervisor that ``pg_autoctl node run`` exec's into watches the ini file +for changes. When it detects a write it re-reads the file and converges any +**mutable** fields without restarting the node or interrupting replication: -``[monitor]`` - ``pguri`` — connection string to the pg_auto_failover monitor. - ``no_monitor = true`` for :ref:`disabled-monitor` mode. - ``node_id`` — required with ``no_monitor``. +``candidate_priority`` and ``replication_quorum`` + Applied by calling ``pg_autoctl set node`` against the running node. -``[formation]`` - ``name`` — formation name, default ``"default"``. - ``group`` — Citus group id (0 = coordinator). +``ssl``, ``ca_file``, ``cert_file``, ``key_file`` + Applied by calling ``pg_autoctl enable ssl`` with the appropriate flags. + Postgres reloads its SSL configuration without a full restart. -``[settings]`` *(mutable — changes take effect without restart)* - ``candidate_priority`` — failover weight 0–100, default 50. - ``replication_quorum`` — sync quorum participant, default ``true``. +Changing an **immutable** field (``kind``, ``pgdata``, ``hostname``, ``port``, +``monitor.pguri``, ``auth``, ``pg_hba_lan``) while the node is running is +logged as a warning; the value takes effect the next time the node is started. -``[options]`` *(create-time only — ignored on restart)* - ``ssl`` — ``self-signed``, ``verify-ca``, ``verify-full``, or ``off``. - ``auth`` — ``trust``, ``md5``, ``scram``, or ``cert``. - ``pg_hba_lan`` — add LAN-range entries to ``pg_hba.conf``. +The ``launch = deferred`` Pattern +---------------------------------- -``[ssl]`` *(for ``verify-ca`` / ``verify-full`` modes)* - ``ssl_ca_file``, ``ssl_cert_file``, ``ssl_key_file``. +:: -``[launch]`` *(optional — for ordered startup)* - ``mode = deferred`` — hold the node in a wait loop until - ``pg_autoctl node start`` writes ``mode = immediate``. - Useful in orchestrators that need fine-grained control over - the order in which nodes join the formation. + [launch] + mode = deferred -``[formation ]`` *(monitor kind only — repeat for each non-default formation)* - ``kind`` — ``pgsql`` (default) or ``citus``. +A node configured with ``mode = deferred`` starts a polling loop and waits. +A sidecar container or init script then calls:: -Live Reconfiguration --------------------- + pg_autoctl node start /etc/pgaf/node.ini -The supervisor that ``pg_autoctl node run`` exec's into watches the ini file -for changes. When it detects a write (via inotify on Linux, mtime polling -elsewhere) it re-reads the ``[settings]`` section and applies any changes -without restarting the node or interrupting replication. - -Fields that are **mutable** and applied live: - -- ``candidate_priority`` -- ``replication_quorum`` - -Fields that are **immutable** (require a node restart to take effect): -``kind``, ``pgdata``, ``hostname``, ``port``, ``monitor.pguri``, all -``[options]`` and ``[ssl]`` values. - -Changing an immutable field while the node is running is logged as a warning; -the new value will take effect the next time the node is started. - -Docker and Kubernetes Usage ---------------------------- - -The fixed default path ``/etc/pgaf/node.ini`` lets every container image -use the same entry-point:: - - CMD ["pg_autoctl", "node", "run", "/etc/pgaf/node.ini"] - -Per-node configuration is then a bind-mount (Docker) or a ConfigMap -volume (Kubernetes), keeping the image itself fully generic. - -**Docker Compose example:** - -.. code-block:: yaml - - services: - monitor: - image: hapostgres/pg_auto_failover:latest - volumes: - - ./config/monitor.ini:/etc/pgaf/node.ini:ro - command: ["pg_autoctl", "node", "run", "/etc/pgaf/node.ini"] - - node1: - image: hapostgres/pg_auto_failover:latest - volumes: - - node1-data:/var/lib/postgresql/data - - ./config/node1.ini:/etc/pgaf/node.ini:ro - command: ["pg_autoctl", "node", "run", "/etc/pgaf/node.ini"] - depends_on: [monitor] - - node2: - image: hapostgres/pg_auto_failover:latest - volumes: - - node2-data:/var/lib/postgresql/data - - ./config/node2.ini:/etc/pgaf/node.ini:ro - command: ["pg_autoctl", "node", "run", "/etc/pgaf/node.ini"] - depends_on: [monitor] - - volumes: - node1-data: - node2-data: - -To promote ``node2`` to a higher failover priority without restarting it, -edit ``node2.ini`` and change ``candidate_priority = 80``, then write the -file. The supervisor picks up the change within seconds. - -**Kubernetes StatefulSet example:** - -.. code-block:: yaml - - apiVersion: apps/v1 - kind: StatefulSet - metadata: - name: pg-node - spec: - replicas: 2 - template: - spec: - containers: - - name: pg-autoctl - image: hapostgres/pg_auto_failover:latest - command: ["pg_autoctl", "node", "run", "/etc/pgaf/node.ini"] - volumeMounts: - - name: node-spec - mountPath: /etc/pgaf - volumes: - - name: node-spec - configMap: - name: pg-autoctl-node-spec - -Updating the ConfigMap triggers the supervisor's file watcher and mutable -settings converge automatically; immutable changes require a pod restart. - -Relationship to ``pg_autoctl create`` and ``pg_autoctl run`` ------------------------------------------------------------- - -``pg_autoctl node run`` is a thin layer on top of the existing machinery — -it translates the ini file into the same flags and exec's into the same -supervisor that ``pg_autoctl create ... --run`` or ``pg_autoctl run`` would -start. There is no hidden API: every behaviour described here maps directly -to documented ``pg_autoctl`` operations. - -You can always switch between approaches: - -- Use ``pg_autoctl node show --pgdata `` to generate an ini file from - an existing node that was created with ``pg_autoctl create``. -- Use ``pg_autoctl create`` and ``pg_autoctl run`` directly when you prefer - explicit flag-based management. - -Both approaches share the same state files, configuration, and monitor -protocol — only the entry point differs. +which rewrites the ini file with ``mode = immediate``. The waiting node +detects the change within the poll interval and proceeds to create or run. +This enables ordered startup without an external orchestrator: the monitor +container can be given ``mode = immediate`` while all data nodes start with +``mode = deferred``, and each data node is released with ``node start`` only +after the monitor is confirmed ready. See Also -------- diff --git a/src/bin/pg_autoctl/nodespec.c b/src/bin/pg_autoctl/nodespec.c index 54d142c82..d9831dbb1 100644 --- a/src/bin/pg_autoctl/nodespec.c +++ b/src/bin/pg_autoctl/nodespec.c @@ -114,7 +114,8 @@ nodespec_read(const char *path, NodeSpec *spec) sizeof(replicationQuorumStr), replicationQuorumStr, "true"), - /* [options] — immutable, used only at create time */ + /* [options] — ssl is mutable (applied via `pg_autoctl enable ssl`); + auth and pg_hba_lan are create-time only */ make_strbuf_option_default("options", "ssl", NULL, false, sizeof(spec->ssl), spec->ssl, "self-signed"), @@ -683,15 +684,17 @@ nodespec_write_to_path(const NodeSpec *spec, const char *path) * nodespec_apply compares new_spec against old_spec and applies any changes * to the mutable fields by calling into the keeper / monitor APIs. * - * Currently mutable fields: - * - candidate_priority → monitor_set_node_candidate_priority() - * - replication_quorum → monitor_set_node_replication_quorum() + * Mutable fields: + * - candidate_priority → pg_autoctl set node candidate-priority + * - replication_quorum → pg_autoctl set node replication-quorum + * - ssl / ssl_*_file → pg_autoctl enable ssl + * + * Immutable fields (kind, pgdata, hostname, port, monitor_pguri, + * auth, pg_hba_lan) require a node restart to take effect. * * The [launch] mode field is handled separately by pg_autoctl node start. * Applying a spec with mode=deferred to an already-started node is a * non-fatal warning (ignored). - * - * Immutable fields (kind, pgdata, ssl, auth, pg_hba_lan) are not checked here. */ bool nodespec_apply(const NodeSpec *new_spec, const NodeSpec *old_spec) @@ -752,6 +755,69 @@ nodespec_apply(const NodeSpec *new_spec, const NodeSpec *old_spec) free_program(&prog); } + /* + * SSL mode or certificate paths changed: call `pg_autoctl enable ssl` + * with the appropriate flags so Postgres is reconfigured live. + * + * ssl=off maps to --no-ssl; ssl=self-signed maps to --ssl-self-signed; + * anything else is a CA-verified mode and requires the cert paths. + */ + { + bool ssl_changed = + (strcmp(new_spec->ssl, old_spec->ssl) != 0) || + (strcmp(new_spec->ssl_ca_file, old_spec->ssl_ca_file) != 0) || + (strcmp(new_spec->ssl_cert_file, old_spec->ssl_cert_file) != 0) || + (strcmp(new_spec->ssl_key_file, old_spec->ssl_key_file) != 0); + + if (ssl_changed) + { + Program prog; + + if (strcmp(new_spec->ssl, "off") == 0) + { + prog = run_program(pg_autoctl_program, + "enable", "ssl", + "--pgdata", new_spec->pgdata, + "--no-ssl", NULL); + } + else if (strcmp(new_spec->ssl, "self-signed") == 0 || + IS_EMPTY_STRING_BUFFER(new_spec->ssl_ca_file)) + { + prog = run_program(pg_autoctl_program, + "enable", "ssl", + "--pgdata", new_spec->pgdata, + "--ssl-self-signed", NULL); + } + else + { + prog = run_program(pg_autoctl_program, + "enable", "ssl", + "--pgdata", new_spec->pgdata, + "--ssl-mode", new_spec->ssl, + "--ssl-ca-file", new_spec->ssl_ca_file, + "--server-cert", new_spec->ssl_cert_file, + "--server-key", new_spec->ssl_key_file, + NULL); + } + + if (prog.returnCode != 0) + { + log_warn("nodespec_apply: enable ssl failed (rc=%d)", + prog.returnCode); + if (prog.stdOut) + { + log_warn("%s", prog.stdOut); + } + } + else + { + log_info("nodespec: applied ssl = %s", new_spec->ssl); + changed = true; + } + free_program(&prog); + } + } + /* immediate → deferred on an already-started node: non-fatal, ignored */ if (!old_spec->launchDeferred && new_spec->launchDeferred) { From 245513929d8f1453c1624db0e40be4a6fd2bf839 Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Mon, 6 Jul 2026 23:31:31 +0200 Subject: [PATCH 03/68] nodespec: make monitor_pguri mutable via disable/enable monitor When the [monitor] pguri changes in pg_autoctl_node.ini, nodespec_apply now re-registers the node to the new monitor without stopping Postgres: pg_autoctl disable monitor --force --pgdata pg_autoctl enable monitor --pgdata The disable step removes the node from the old monitor (--force allows this even if the old monitor is temporarily unreachable). The enable step registers the node to the new monitor and signals the running supervisor to start using the new monitor_pguri for node_active calls. Document the change in the property table and Live Reconfiguration section of pg_autoctl_node.rst. --- docs/ref/pg_autoctl_node.rst | 16 ++++++++-- src/bin/pg_autoctl/nodespec.c | 59 +++++++++++++++++++++++++++++++++-- src/bin/pg_autoctl/nodespec.h | 4 ++- 3 files changed, 73 insertions(+), 6 deletions(-) diff --git a/docs/ref/pg_autoctl_node.rst b/docs/ref/pg_autoctl_node.rst index 161e0cee9..ce57519ea 100644 --- a/docs/ref/pg_autoctl_node.rst +++ b/docs/ref/pg_autoctl_node.rst @@ -133,9 +133,12 @@ on a running node without a restart, and its default value. - * - ``pguri`` - connstring - - No + - **Yes** - — - Connection string to the pg_auto_failover monitor. Empty for monitor nodes. + Changing it re-registers the node to the new monitor without restarting + Postgres (``pg_autoctl disable monitor --force`` then + ``pg_autoctl enable monitor ``). * - ``no_monitor`` - boolean - No @@ -266,9 +269,16 @@ for changes. When it detects a write it re-reads the file and converges any Applied by calling ``pg_autoctl enable ssl`` with the appropriate flags. Postgres reloads its SSL configuration without a full restart. +``monitor.pguri`` + Applied by calling ``pg_autoctl disable monitor --force`` (which removes + the node from the old monitor) followed by + ``pg_autoctl enable monitor `` (which registers the node to the + new monitor). Postgres keeps running throughout; only the monitoring + relationship changes. + Changing an **immutable** field (``kind``, ``pgdata``, ``hostname``, ``port``, -``monitor.pguri``, ``auth``, ``pg_hba_lan``) while the node is running is -logged as a warning; the value takes effect the next time the node is started. +``auth``, ``pg_hba_lan``) while the node is running is logged as a warning; +the value takes effect the next time the node is started. The ``launch = deferred`` Pattern ---------------------------------- diff --git a/src/bin/pg_autoctl/nodespec.c b/src/bin/pg_autoctl/nodespec.c index d9831dbb1..2a30c46dc 100644 --- a/src/bin/pg_autoctl/nodespec.c +++ b/src/bin/pg_autoctl/nodespec.c @@ -688,9 +688,11 @@ nodespec_write_to_path(const NodeSpec *spec, const char *path) * - candidate_priority → pg_autoctl set node candidate-priority * - replication_quorum → pg_autoctl set node replication-quorum * - ssl / ssl_*_file → pg_autoctl enable ssl + * - monitor_pguri → pg_autoctl disable monitor --force + * pg_autoctl enable monitor * - * Immutable fields (kind, pgdata, hostname, port, monitor_pguri, - * auth, pg_hba_lan) require a node restart to take effect. + * Immutable fields (kind, pgdata, hostname, port, auth, + * pg_hba_lan) require a node restart to take effect. * * The [launch] mode field is handled separately by pg_autoctl node start. * Applying a spec with mode=deferred to an already-started node is a @@ -755,6 +757,59 @@ nodespec_apply(const NodeSpec *new_spec, const NodeSpec *old_spec) free_program(&prog); } + /* + * Monitor URI changed: disable the current monitor (removing this node + * from it) then re-register to the new one. The --force flag allows + * disable to proceed even if the old monitor is unreachable. + */ + if (strcmp(new_spec->monitor_pguri, old_spec->monitor_pguri) != 0 && + !IS_EMPTY_STRING_BUFFER(new_spec->monitor_pguri)) + { + Program disable_prog = run_program(pg_autoctl_program, + "disable", "monitor", + "--force", + "--pgdata", new_spec->pgdata, + NULL); + + if (disable_prog.returnCode != 0) + { + log_warn("nodespec_apply: disable monitor failed (rc=%d)", + disable_prog.returnCode); + if (disable_prog.stdOut) + { + log_warn("%s", disable_prog.stdOut); + } + free_program(&disable_prog); + } + else + { + free_program(&disable_prog); + + Program enable_prog = run_program(pg_autoctl_program, + "enable", "monitor", + "--pgdata", new_spec->pgdata, + new_spec->monitor_pguri, + NULL); + + if (enable_prog.returnCode != 0) + { + log_warn("nodespec_apply: enable monitor \"%s\" failed (rc=%d)", + new_spec->monitor_pguri, enable_prog.returnCode); + if (enable_prog.stdOut) + { + log_warn("%s", enable_prog.stdOut); + } + } + else + { + log_info("nodespec: applied monitor_pguri = %s", + new_spec->monitor_pguri); + changed = true; + } + free_program(&enable_prog); + } + } + /* * SSL mode or certificate paths changed: call `pg_autoctl enable ssl` * with the appropriate flags so Postgres is reconfigured live. diff --git a/src/bin/pg_autoctl/nodespec.h b/src/bin/pg_autoctl/nodespec.h index 3328643f1..128ef15fb 100644 --- a/src/bin/pg_autoctl/nodespec.h +++ b/src/bin/pg_autoctl/nodespec.h @@ -59,7 +59,9 @@ typedef struct NodeSpec /* [postgresql] */ char pgdata[MAXPGPATH]; - /* [monitor] — empty for kind == monitor */ + /* [monitor] — empty for kind == monitor + * monitor_pguri is mutable: changing it triggers disable monitor --force + * followed by enable monitor (re-registers without restarting). */ char monitor_pguri[MAXCONNINFO]; bool noMonitor; /* [monitor] no_monitor=true: standalone mode */ int nodeId; /* [monitor] node_id: required with --disable-monitor */ From 6a860c2b2c372f6834753594f1133f3e3655a8d6 Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Mon, 6 Jul 2026 23:34:54 +0200 Subject: [PATCH 04/68] docs: complete pg_autoctl node sub-command pages, remove Container section Follow the pg_autoctl_create pattern: each pg_autoctl node sub-command now has its own manual page, all listed in pg_autoctl_node.rst's toctree: pg_autoctl node run (existing) pg_autoctl node apply (new) pg_autoctl node start (new) pg_autoctl node show (new) pg_autoctl node check (new) Remove the redundant 'Container and Kubernetes Deployments' section from operations.rst. The full documentation for this feature lives in the pg_autoctl_node manual page (ref/pg_autoctl_node.rst) and its sub-pages. --- docs/operations.rst | 51 --------------------------- docs/ref/pg_autoctl_node.rst | 4 +++ docs/ref/pg_autoctl_node_apply.rst | 55 ++++++++++++++++++++++++++++++ docs/ref/pg_autoctl_node_check.rst | 40 ++++++++++++++++++++++ docs/ref/pg_autoctl_node_show.rst | 40 ++++++++++++++++++++++ docs/ref/pg_autoctl_node_start.rst | 49 ++++++++++++++++++++++++++ 6 files changed, 188 insertions(+), 51 deletions(-) create mode 100644 docs/ref/pg_autoctl_node_apply.rst create mode 100644 docs/ref/pg_autoctl_node_check.rst create mode 100644 docs/ref/pg_autoctl_node_show.rst create mode 100644 docs/ref/pg_autoctl_node_start.rst diff --git a/docs/operations.rst b/docs/operations.rst index c304eb128..6149100d9 100644 --- a/docs/operations.rst +++ b/docs/operations.rst @@ -440,54 +440,3 @@ time -- for instance because a firewall rule hasn't yet activated -- it's possible to try ``pg_autoctl create`` again. pg_auto_failover will review its previous progress and repeat idempotent operations (``create database``, ``create extension`` etc), gracefully handling errors. - -Container and Kubernetes Deployments -------------------------------------- - -``pg_autoctl node`` provides a declarative, file-driven entry-point designed -for container and Kubernetes environments. Instead of assembling long -``pg_autoctl create`` flag sequences per node, the complete node description -lives in a single ``pg_autoctl_node.ini`` file: - -.. code-block:: ini - - [node] - kind = postgres - hostname = node1.internal - port = 5432 - - [postgresql] - pgdata = /var/lib/postgresql/data - - [monitor] - pguri = postgres://autoctl_node@monitor:5432/pg_auto_failover - - [settings] - candidate_priority = 50 - replication_quorum = true - - [options] - ssl = self-signed - -A single command then creates the node if absent and starts the supervisor:: - - pg_autoctl node run /etc/pgaf/node.ini - -This makes ``pg_autoctl node run`` a natural ``CMD`` (Docker) or -``command:`` (Kubernetes) for every node type. The same image works for -monitor, primary, standby, coordinator, and worker nodes — per-node -differences live entirely in the bind-mounted ini file. - -**Live reconfiguration** — the supervisor watches the ini file. Editing -``candidate_priority``, ``replication_quorum``, or the ``ssl`` settings and -saving the file is sufficient to converge the running node; no restart is -required. - -**Ordered startup** — add ``[launch] mode = deferred`` to any node that -should wait for an external signal before initialising. Call -``pg_autoctl node start `` from a sidecar or init container to release -it. This replaces external orchestration for the common case where data -nodes must wait until the monitor is ready. - -For the full property reference, mutability table, and Compose / Kubernetes -examples see :ref:`pg_autoctl_node`. diff --git a/docs/ref/pg_autoctl_node.rst b/docs/ref/pg_autoctl_node.rst index ce57519ea..2d3c29956 100644 --- a/docs/ref/pg_autoctl_node.rst +++ b/docs/ref/pg_autoctl_node.rst @@ -23,6 +23,10 @@ creation, startup, and live reconfiguration — driven by a single :maxdepth: 1 pg_autoctl_node_run + pg_autoctl_node_apply + pg_autoctl_node_start + pg_autoctl_node_show + pg_autoctl_node_check Description ----------- diff --git a/docs/ref/pg_autoctl_node_apply.rst b/docs/ref/pg_autoctl_node_apply.rst new file mode 100644 index 000000000..ea3cf2986 --- /dev/null +++ b/docs/ref/pg_autoctl_node_apply.rst @@ -0,0 +1,55 @@ +.. _pg_autoctl_node_apply: + +pg_autoctl node apply +===================== + +pg_autoctl node apply - Apply mutable settings from a pg_autoctl_node.ini to a running node + +Synopsis +-------- + +:: + + pg_autoctl node apply [] + + path to the pg_autoctl_node.ini file + (default: /etc/pgaf/node.ini) + +Description +----------- + +``pg_autoctl node apply`` reads a ``pg_autoctl_node.ini`` file, compares its +mutable fields against the node's current configuration, and converges any +differences without restarting the node or interrupting replication. + +This is the same convergence logic the supervisor runs automatically when it +detects a file change. Running ``pg_autoctl node apply`` manually is useful +when the supervisor is not running, or when you want to apply a change +immediately rather than waiting for the next watch interval. + +Mutable fields applied by this command: + +``candidate_priority`` + Applied via :ref:`pg_autoctl_set_node_candidate_priority`. + +``replication_quorum`` + Applied via :ref:`pg_autoctl_set_node_replication_quorum`. + +``ssl``, ``ca_file``, ``cert_file``, ``key_file`` + Applied via ``pg_autoctl enable ssl``. Postgres reloads its SSL + configuration without a full restart. + +``monitor.pguri`` + Applied via ``pg_autoctl disable monitor --force`` followed by + ``pg_autoctl enable monitor ``. The node is re-registered to + the new monitor without stopping Postgres. + +Immutable fields (``kind``, ``pgdata``, ``hostname``, ``port``, +``auth``, ``pg_hba_lan``) are logged as warnings when they differ; they take +effect only the next time the node is created or started from scratch. + +See Also +-------- + +:ref:`pg_autoctl_node`, :ref:`pg_autoctl_node_run`, +:ref:`pg_autoctl_set`, :ref:`pg_autoctl_enable` diff --git a/docs/ref/pg_autoctl_node_check.rst b/docs/ref/pg_autoctl_node_check.rst new file mode 100644 index 000000000..deed6b7cf --- /dev/null +++ b/docs/ref/pg_autoctl_node_check.rst @@ -0,0 +1,40 @@ +.. _pg_autoctl_node_check: + +pg_autoctl node check +===================== + +pg_autoctl node check - Validate a pg_autoctl_node.ini file without creating anything + +Synopsis +-------- + +:: + + pg_autoctl node check [] + + path to the pg_autoctl_node.ini file + (default: /etc/pgaf/node.ini) + +Description +----------- + +``pg_autoctl node check`` parses a ``pg_autoctl_node.ini`` file, resolves +defaults, and prints the fully-resolved configuration to stdout. It exits +with a non-zero status if the file contains any syntax errors or invalid +field values. + +No Postgres instance is touched and no pg_autoctl state is modified. This +command is safe to run at any time. + +Use ``pg_autoctl node check`` to: + +- Validate a newly written ini file before deploying it to a container. +- Verify that defaults are resolved as expected (for example, that + ``hostname`` is auto-detected correctly). +- Catch errors in CI before reaching the node creation step. + +See Also +-------- + +:ref:`pg_autoctl_node`, :ref:`pg_autoctl_node_run`, +:ref:`pg_autoctl_node_show` diff --git a/docs/ref/pg_autoctl_node_show.rst b/docs/ref/pg_autoctl_node_show.rst new file mode 100644 index 000000000..0a2b6e509 --- /dev/null +++ b/docs/ref/pg_autoctl_node_show.rst @@ -0,0 +1,40 @@ +.. _pg_autoctl_node_show: + +pg_autoctl node show +==================== + +pg_autoctl node show - Dump current node configuration as a pg_autoctl_node.ini file + +Synopsis +-------- + +:: + + pg_autoctl node show [--pgdata ] + + --pgdata path to data directory (default: $PGDATA) + +Description +----------- + +``pg_autoctl node show`` reads the running node's ``pg_autoctl.cfg`` +configuration file and writes its contents to stdout in +``pg_autoctl_node.ini`` format. + +This is useful for: + +- Capturing the current configuration of a node that was created with + ``pg_autoctl create postgres`` flags, so it can be managed declaratively + going forward. +- Comparing the effective configuration of a running node against a desired + ``pg_autoctl_node.ini`` file. +- Generating a baseline ini file to store in version control. + +The output is a valid ``pg_autoctl_node.ini`` file that can be passed +directly to ``pg_autoctl node run`` or ``pg_autoctl node apply``. + +See Also +-------- + +:ref:`pg_autoctl_node`, :ref:`pg_autoctl_node_run`, +:ref:`pg_autoctl_node_check` diff --git a/docs/ref/pg_autoctl_node_start.rst b/docs/ref/pg_autoctl_node_start.rst new file mode 100644 index 000000000..c70718658 --- /dev/null +++ b/docs/ref/pg_autoctl_node_start.rst @@ -0,0 +1,49 @@ +.. _pg_autoctl_node_start: + +pg_autoctl node start +===================== + +pg_autoctl node start - Release a node waiting in launch=deferred mode + +Synopsis +-------- + +:: + + pg_autoctl node start [] + + path to the pg_autoctl_node.ini file + (default: /etc/pgaf/node.ini) + +Description +----------- + +``pg_autoctl node start`` releases a node that is waiting in +``[launch] mode = deferred``. It rewrites the ini file with +``mode = immediate``; the waiting node detects the change within the poll +interval and proceeds to create or run. + +This command is idempotent: calling it on a node that is already running +(or has ``mode = immediate``) is a no-op. + +The ``launch = deferred`` Pattern +---------------------------------- + +A node configured with ``[launch] mode = deferred`` starts a polling loop +and waits instead of immediately creating or starting Postgres. This +enables ordered startup without an external orchestrator:: + + # In the ini file for each data node: + [launch] + mode = deferred + +The monitor can be given ``mode = immediate`` (the default), while data nodes +start with ``mode = deferred``. Once the monitor is confirmed ready, release +each data node:: + + pg_autoctl node start /etc/pgaf/node.ini + +See Also +-------- + +:ref:`pg_autoctl_node`, :ref:`pg_autoctl_node_run` From 2882a0b1b983c2d1d5793c63c1fb7ffe1b637b4b Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Mon, 6 Jul 2026 23:40:35 +0200 Subject: [PATCH 05/68] docs: replace list-table with definition-list style in pg_autoctl_node The list-table rendered poorly in the Sphinx HTML theme. Replace it with the same definition-list style used throughout the other manual pages (e.g. pg_autoctl_create_postgres Options section): each property is a bare term followed by indented description paragraphs, grouped under ini-section sub-headings. --- docs/ref/pg_autoctl_node.rst | 320 +++++++++++++++++------------------ 1 file changed, 151 insertions(+), 169 deletions(-) diff --git a/docs/ref/pg_autoctl_node.rst b/docs/ref/pg_autoctl_node.rst index 2d3c29956..e84a32b44 100644 --- a/docs/ref/pg_autoctl_node.rst +++ b/docs/ref/pg_autoctl_node.rst @@ -83,181 +83,163 @@ A monitor node omits ``[monitor]`` entirely:: Section and Property Reference ------------------------------- -The table below lists every property, its section, whether it can be changed -on a running node without a restart, and its default value. - -.. list-table:: - :widths: 18 14 8 12 48 - :header-rows: 1 - - * - Section / Property - - Type - - Mutable - - Default - - Description - * - **[node]** - - - - - - - - - * - ``kind`` - - string - - No - - — - - Node role: ``postgres``, ``monitor``, ``coordinator``, or ``worker`` - * - ``name`` - - string - - No - - hostname - - Human-readable node name shown in ``pg_autoctl show state`` - * - ``hostname`` - - string - - No - - auto-detected - - Address other nodes use to reach this node - * - ``port`` - - integer - - No - - 5432 - - Postgres port number - * - **[postgresql]** - - - - - - - - - * - ``pgdata`` - - path - - No - - — - - Path to the Postgres data directory - * - **[monitor]** - - - - - - - - - * - ``pguri`` - - connstring - - **Yes** - - — - - Connection string to the pg_auto_failover monitor. Empty for monitor nodes. - Changing it re-registers the node to the new monitor without restarting - Postgres (``pg_autoctl disable monitor --force`` then - ``pg_autoctl enable monitor ``). - * - ``no_monitor`` - - boolean - - No - - false - - Set ``true`` for :ref:`pg_autoctl_disable_monitor` (standalone) mode - * - ``node_id`` - - integer - - No - - — - - Required with ``no_monitor = true`` - * - **[formation]** - - - - - - - - - * - ``name`` - - string - - No - - default - - Formation name - * - ``group`` - - integer - - No - - 0 - - Citus group id (0 = coordinator) - * - **[settings]** *(applied live without restart)* - - - - - - - - - * - ``candidate_priority`` - - integer 0–100 - - **Yes** - - 50 - - Failover weight. 0 means never promote this node. - * - ``replication_quorum`` - - boolean - - **Yes** - - true - - Whether this node participates in the synchronous replication quorum - * - **[options]** - - - - - - - - - * - ``ssl`` - - string - - **Yes** - - self-signed - - SSL mode: ``self-signed``, ``verify-ca``, ``verify-full``, or ``off``. - Changes call ``pg_autoctl enable ssl`` on the running node. - * - ``auth`` - - string - - No - - trust - - Authentication method written into ``pg_hba.conf`` at create time: - ``trust``, ``md5``, ``scram``, or ``cert`` - * - ``pg_hba_lan`` - - boolean - - No - - true - - Add LAN-range entries to ``pg_hba.conf`` at create time - * - **[ssl]** *(for ``verify-ca`` / ``verify-full`` modes; applied live)* - - - - - - - - - * - ``ca_file`` - - path - - **Yes** - - — - - Path to the CA certificate file (``ssl_ca_file`` in postgresql.conf) - * - ``cert_file`` - - path - - **Yes** - - — - - Path to the server certificate (``ssl_cert_file``) - * - ``key_file`` - - path - - **Yes** - - — - - Path to the server private key (``ssl_key_file``) - * - **[launch]** *(optional)* - - - - - - - - - * - ``mode`` - - string - - — - - immediate - - ``deferred``: hold the node in a wait loop until ``pg_autoctl node start`` - writes ``immediate``. Useful for ordered startup in compose or k8s. - * - **[formation ]** *(monitor kind only, repeat for each extra formation)* - - - - - - - - - * - ``kind`` - - string - - No - - pgsql - - Formation kind: ``pgsql`` or ``citus`` - -Additional sections for passwords (kept out of the main ini where possible): +The following sections and properties are supported in +``pg_autoctl_node.ini``. Properties marked *mutable* can be changed on a +running node by editing the file; the supervisor converges the change without +restarting Postgres. Immutable properties take effect only the next time the +node is created or started from scratch. + +``[node]`` +^^^^^^^^^^ + +``kind`` + + Node role. One of ``postgres``, ``monitor``, ``coordinator``, or + ``worker``. Required; immutable. + +``name`` + + Human-readable node name shown in ``pg_autoctl show state``. Defaults to + the value of ``hostname`` when not set. Immutable. + +``hostname`` + + Address that other nodes (including the monitor) use to reach this node. + Defaults to the auto-detected FQDN of the host. Immutable. + +``port`` + + Postgres port number. Defaults to ``5432``. Immutable. + +``[postgresql]`` +^^^^^^^^^^^^^^^^ + +``pgdata`` + + Path to the Postgres data directory. Required; immutable. + +``[monitor]`` +^^^^^^^^^^^^^ + +``pguri`` + + Connection string to the pg_auto_failover monitor. Omit for monitor + nodes. **Mutable**: changing this value re-registers the node to the new + monitor without restarting Postgres (``pg_autoctl disable monitor --force`` + followed by ``pg_autoctl enable monitor ``). + +``no_monitor`` + + Set to ``true`` to run in :ref:`pg_autoctl_disable_monitor` (standalone) + mode. Immutable. + +``node_id`` + + Node identifier to use when ``no_monitor = true``. Immutable. + +``[formation]`` +^^^^^^^^^^^^^^^ + +``name`` + + Formation name. Defaults to ``default``. Immutable. + +``group`` + + Citus group identifier. ``0`` means coordinator. Defaults to ``0``. + Immutable. + +``[settings]`` +^^^^^^^^^^^^^^ + +Settings in this section are applied live without restarting the node. + +``candidate_priority`` + + Failover weight, an integer from 0 to 100. A value of ``0`` means this + node is never promoted to primary. Defaults to ``50``. **Mutable**. + +``replication_quorum`` + + Whether this node participates in the synchronous replication quorum. + Boolean, defaults to ``true``. **Mutable**. + +``[options]`` +^^^^^^^^^^^^^ + +``ssl`` + + SSL mode. One of ``self-signed``, ``verify-ca``, ``verify-full``, or + ``off``. Defaults to ``self-signed``. **Mutable**: changes are applied via + ``pg_autoctl enable ssl``; Postgres reloads SSL without a full restart. + +``auth`` + + Authentication method written into ``pg_hba.conf`` at create time. One of + ``trust``, ``md5``, ``scram``, or ``cert``. Defaults to ``trust``. + Immutable (create-time only). + +``pg_hba_lan`` + + When ``true``, add LAN-range entries to ``pg_hba.conf`` at create time. + Defaults to ``true``. Immutable (create-time only). + +``[ssl]`` +^^^^^^^^^ + +Certificate paths for ``verify-ca`` and ``verify-full`` SSL modes. All +three are **mutable**: editing them and saving the file reconfigures Postgres +SSL live via ``pg_autoctl enable ssl``. + +``ca_file`` + + Path to the CA certificate file (``ssl_ca_file`` in ``postgresql.conf``). + +``cert_file`` + + Path to the server certificate file (``ssl_cert_file``). + +``key_file`` + + Path to the server private key file (``ssl_key_file``). + +``[launch]`` +^^^^^^^^^^^^ + +``mode`` + + When set to ``deferred``, the node starts a polling loop and waits instead + of creating or starting Postgres immediately. Call ``pg_autoctl node + start`` to release it. Defaults to ``immediate``. See + :ref:`pg_autoctl_node_start`. + +``[formation ]`` +^^^^^^^^^^^^^^^^^^^^^^ + +Repeat this section for each non-default formation to create. Applies to +monitor nodes only. + +``kind`` + + Formation kind. One of ``pgsql`` or ``citus``. Defaults to ``pgsql``. + Immutable. + +Password sections +^^^^^^^^^^^^^^^^^ + +These sections hold credentials that are kept out of the main ini file where +possible. ``[pg_auto_failover]`` - ``autoctl_node_password`` — password for the ``pgautofailover_monitor`` role - (monitor nodes), or the ``autoctl_node`` role (data nodes). + ``autoctl_node_password`` — password for the ``pgautofailover_monitor`` + role (monitor nodes) or the ``autoctl_node`` role (data nodes). ``[replication]`` - ``replication_password`` — password for the ``pgautofailover_replicator`` role. + ``replication_password`` — password for the ``pgautofailover_replicator`` + role. ``[citus]`` - ``role = secondary`` and ``cluster_name`` for Citus secondary clusters. + ``role = secondary`` and ``cluster_name`` for Citus secondary clusters. Live Reconfiguration -------------------- From e504fafd515f5d7c737c6a0773733a6f3e96f82f Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Mon, 6 Jul 2026 23:42:20 +0200 Subject: [PATCH 06/68] docs: restore Container/K8s section in operations.rst, note in Provisioning Re-add the 'Container and Kubernetes Deployments' section at the end of operations.rst with a named anchor so it can be referenced from elsewhere. Add a short note in the Provisioning section pointing to pg_autoctl node run as the declarative alternative for container and Kubernetes deployments, with a cross-reference down to the new section. --- docs/operations.rst | 58 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/docs/operations.rst b/docs/operations.rst index 6149100d9..60a6566a0 100644 --- a/docs/operations.rst +++ b/docs/operations.rst @@ -19,6 +19,13 @@ pg_auto_failover monitor. The ``pg_autoctl create`` command honors the running. If Postgres is detected, the new node is registered in SINGLE mode, bypassing the monitor's role assignment policy. +For container and Kubernetes environments, :ref:`pg_autoctl_node_run` +provides a declarative alternative. The complete node description lives in +a single ``pg_autoctl_node.ini`` file that is bind-mounted into the +container; ``pg_autoctl node run`` creates the node if absent and starts the +supervisor in one step, making it a natural ``CMD`` / ``command:`` entry-point +for every node type. See `Container and Kubernetes Deployments`_ below. + Postgres configuration management --------------------------------- @@ -440,3 +447,54 @@ time -- for instance because a firewall rule hasn't yet activated -- it's possible to try ``pg_autoctl create`` again. pg_auto_failover will review its previous progress and repeat idempotent operations (``create database``, ``create extension`` etc), gracefully handling errors. + +.. _container-and-kubernetes-deployments: + +Container and Kubernetes Deployments +------------------------------------- + +:ref:`pg_autoctl_node` provides a declarative, file-driven entry-point +designed for container and Kubernetes environments. Instead of assembling +long ``pg_autoctl create`` flag sequences per node, the complete node +description lives in a single ``pg_autoctl_node.ini`` file:: + + [node] + kind = postgres + hostname = node1.internal + port = 5432 + + [postgresql] + pgdata = /var/lib/postgresql/data + + [monitor] + pguri = postgres://autoctl_node@monitor:5432/pg_auto_failover + + [settings] + candidate_priority = 50 + replication_quorum = true + + [options] + ssl = self-signed + +A single command then creates the node if absent and starts the supervisor:: + + pg_autoctl node run /etc/pgaf/node.ini + +This makes ``pg_autoctl node run`` a natural ``CMD`` (Docker) or +``command:`` (Kubernetes) entry-point for every node type. The same image +works for monitor, primary, standby, coordinator, and worker nodes — per-node +differences live entirely in the bind-mounted ini file. + +**Live reconfiguration** — the supervisor watches the ini file. Editing +``candidate_priority``, ``replication_quorum``, ``ssl`` settings, or +``monitor.pguri`` and saving the file is sufficient to converge the running +node; no restart is required. + +**Ordered startup** — add ``[launch] mode = deferred`` to any node that +should wait for an external signal before initialising. Call +``pg_autoctl node start `` from a sidecar or init container to release +it. This replaces external orchestration for the common case where data +nodes must wait until the monitor is ready. + +For the full property reference and mutability table see +:ref:`pg_autoctl_node`. From 466f812b6f5a7b9e5a4de1f13a63a93d28a6f043 Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Mon, 6 Jul 2026 23:52:19 +0200 Subject: [PATCH 07/68] docs: add pg_autoctl node cross-references throughout the manual MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add references to pg_autoctl node / pg_autoctl node run in all the places identified by the documentation audit: Reference pages (new See Also section): - pg_autoctl_create_postgres.rst - pg_autoctl_create_monitor.rst - pg_autoctl_create_coordinator.rst - pg_autoctl_create_worker.rst - pg_autoctl_run.rst Narrative docs (short note pointing to the declarative alternative): - how-to.rst — after the pg_autoctl run step in Quick Start - tutorial.rst — after the docker-compose.yml literalinclude - citus-quickstart.rst — after the docker-compose-scale.yml literalinclude - install.rst — after the systemd unit section --- docs/citus-quickstart.rst | 10 ++++++++++ docs/how-to.rst | 4 ++++ docs/install.rst | 7 +++++++ docs/ref/pg_autoctl_create_coordinator.rst | 9 +++++++++ docs/ref/pg_autoctl_create_monitor.rst | 8 ++++++++ docs/ref/pg_autoctl_create_postgres.rst | 8 ++++++++ docs/ref/pg_autoctl_create_worker.rst | 8 ++++++++ docs/ref/pg_autoctl_run.rst | 8 ++++++++ docs/tutorial.rst | 9 +++++++++ 9 files changed, 71 insertions(+) diff --git a/docs/citus-quickstart.rst b/docs/citus-quickstart.rst index e0e2868b7..738b3fdac 100644 --- a/docs/citus-quickstart.rst +++ b/docs/citus-quickstart.rst @@ -57,6 +57,16 @@ To create a cluster we use the following docker compose definition: :emphasize-lines: 5,15,27 :linenos: +.. note:: + + The Compose file above uses the imperative ``pg_autoctl create + coordinator/worker --run`` form to keep the tutorial self-contained. For + production container deployments the recommended approach is + :ref:`pg_autoctl_node_run`: each node is described in a + ``pg_autoctl_node.ini`` file (with ``kind = coordinator`` or + ``kind = worker``) and ``pg_autoctl node run`` handles both creation and + startup in one step. See :ref:`pg_autoctl_node` for the full reference. + To run the full Citus cluster with HA from this definition, we can use the following command: diff --git a/docs/how-to.rst b/docs/how-to.rst index f99f61f09..e9720a7a0 100644 --- a/docs/how-to.rst +++ b/docs/how-to.rst @@ -59,6 +59,10 @@ you can manually run the following command on every node:: $ pg_autoctl run +For container and Kubernetes deployments, :ref:`pg_autoctl_node_run` +combines the create and run steps into a single command driven by a +``pg_autoctl_node.ini`` file — see :ref:`pg_autoctl_node` for details. + It is also possible (and recommended) to integrate the pg_auto_failover service in your usual service management facility. When using **systemd** the following commands can be used to produce the unit file configuration diff --git a/docs/install.rst b/docs/install.rst index 372550c16..0a69ac790 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -106,6 +106,13 @@ systemd itself, as it might be that a failover has been done during a reboot, for instance, and that once the reboot complete we want the local Postgres to re-join as a secondary node where it used to be a primary node. +For container and Kubernetes deployments, systemd is not used. Instead, +:ref:`pg_autoctl_node_run` acts as PID 1: it creates the node if absent, +then exec's into the supervisor. The standard Unix signal contract +(``SIGTERM`` to stop, ``SIGHUP`` to reload) is preserved because the +supervisor becomes the direct child process. See :ref:`pg_autoctl_node` +for the full reference. + Building pg_auto_failover from sources -------------------------------------- diff --git a/docs/ref/pg_autoctl_create_coordinator.rst b/docs/ref/pg_autoctl_create_coordinator.rst index 40cf8f914..e5da597e6 100644 --- a/docs/ref/pg_autoctl_create_coordinator.rst +++ b/docs/ref/pg_autoctl_create_coordinator.rst @@ -90,3 +90,12 @@ options. This section now lists the options that are specific to are part of this cluster. See :ref:`citus_secondaries` for more information. + +See Also +-------- + +:ref:`pg_autoctl_node_run` provides a declarative alternative to this +command: set ``kind = coordinator`` in a ``pg_autoctl_node.ini`` file and +run ``pg_autoctl node run`` — it creates the coordinator if absent and starts +the supervisor in one step. See :ref:`pg_autoctl_node` for the full +reference. diff --git a/docs/ref/pg_autoctl_create_monitor.rst b/docs/ref/pg_autoctl_create_monitor.rst index f65c5d6e9..c3a5a73e0 100644 --- a/docs/ref/pg_autoctl_create_monitor.rst +++ b/docs/ref/pg_autoctl_create_monitor.rst @@ -201,3 +201,11 @@ XDG_DATA_HOME __ https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html + +See Also +-------- + +:ref:`pg_autoctl_node_run` provides a declarative alternative to this +command: set ``kind = monitor`` in a ``pg_autoctl_node.ini`` file and run +``pg_autoctl node run`` — it creates the monitor if absent and starts the +supervisor in one step. See :ref:`pg_autoctl_node` for the full reference. diff --git a/docs/ref/pg_autoctl_create_postgres.rst b/docs/ref/pg_autoctl_create_postgres.rst index e800d1d0f..b2d8b2089 100644 --- a/docs/ref/pg_autoctl_create_postgres.rst +++ b/docs/ref/pg_autoctl_create_postgres.rst @@ -370,3 +370,11 @@ XDG_DATA_HOME Base Directory Specification`__. __ https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html + +See Also +-------- + +:ref:`pg_autoctl_node_run` provides a declarative alternative to this +command: describe the node once in a ``pg_autoctl_node.ini`` file and run +``pg_autoctl node run`` — it creates the node if absent and starts the +supervisor in one step. See :ref:`pg_autoctl_node` for the full reference. diff --git a/docs/ref/pg_autoctl_create_worker.rst b/docs/ref/pg_autoctl_create_worker.rst index daf301e73..6537e56ef 100644 --- a/docs/ref/pg_autoctl_create_worker.rst +++ b/docs/ref/pg_autoctl_create_worker.rst @@ -112,3 +112,11 @@ options. This section now lists the options that are specific to are part of this cluster. See :ref:`citus_secondaries` for more information. + +See Also +-------- + +:ref:`pg_autoctl_node_run` provides a declarative alternative to this +command: set ``kind = worker`` in a ``pg_autoctl_node.ini`` file and run +``pg_autoctl node run`` — it creates the worker if absent and starts the +supervisor in one step. See :ref:`pg_autoctl_node` for the full reference. diff --git a/docs/ref/pg_autoctl_run.rst b/docs/ref/pg_autoctl_run.rst index 08d8650cd..f57f02e79 100644 --- a/docs/ref/pg_autoctl_run.rst +++ b/docs/ref/pg_autoctl_run.rst @@ -87,3 +87,11 @@ Options --pgport Postgres port to use, defaults to 5432. + +See Also +-------- + +:ref:`pg_autoctl_node_run` combines node creation and the service run in a +single command driven by a ``pg_autoctl_node.ini`` file — it is the +recommended entry-point for new deployments, especially in container and +Kubernetes environments. diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 881058957..6394021bb 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -67,6 +67,15 @@ To create a cluster we use the following docker compose definition: :language: yaml :linenos: +.. note:: + + The Compose file above uses the imperative ``pg_autoctl create postgres + --run`` form to keep the tutorial self-contained. For production + container deployments the recommended approach is :ref:`pg_autoctl_node_run`: + each node is described in a ``pg_autoctl_node.ini`` file and the command + ``pg_autoctl node run`` handles both creation and startup in one step. + See :ref:`pg_autoctl_node` for the full reference. + To run the full Citus cluster with HA from this definition, we can use the following command: From b77609a7e0ce5d817c06a5ca0b607d74278ac10f Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Mon, 6 Jul 2026 23:56:22 +0200 Subject: [PATCH 08/68] docs: rewrite tutorial to use pg_autoctl node run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the imperative pg_autoctl create postgres / pg_autoctl create monitor commands with the declarative pg_autoctl node run approach: - Add tutorial/ini/monitor.ini and tutorial/ini/postgres.ini — two small ini files that describe the monitor and every data node respectively. Data nodes share one ini file; hostname and name default to the container hostname set by Docker Compose. - Rewrite tutorial/docker-compose.yml: all PG_AUTOCTL_* environment variables are gone; each service bind-mounts its ini file at /etc/pgaf/node.ini and runs 'pg_autoctl node run'. The x-node anchor is now clean — no env vars, single command. - Update tutorial.rst to introduce the ini files before the compose file, explain that pg_autoctl node run handles both create and run, and show that live reconfiguration (candidate_priority change for node3) is done by editing the ini file rather than calling pg_autoctl set. - Replace the stale pg_autoctl_do_tmux_compose_session reference in Next steps with a pointer to the pg_autoctl_node reference and the Container and Kubernetes Deployments section. --- docs/tutorial.rst | 111 ++++++++++++++++++++----------- docs/tutorial/docker-compose.yml | 20 ++---- docs/tutorial/ini/monitor.ini | 10 +++ docs/tutorial/ini/postgres.ini | 18 +++++ 4 files changed, 104 insertions(+), 55 deletions(-) create mode 100644 docs/tutorial/ini/monitor.ini create mode 100644 docs/tutorial/ini/postgres.ini diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 6394021bb..2eb7d7705 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -61,40 +61,45 @@ provide with High Availability of both the Postgres service and the data. See the :ref:`multi_node_architecture` chapter of our docs to understand more about this. -To create a cluster we use the following docker compose definition: +Each node in the cluster is described by a small ``pg_autoctl_node.ini`` +file that is bind-mounted into its container. There are two files: -.. literalinclude:: tutorial/docker-compose.yml - :language: yaml - :linenos: +.. literalinclude:: tutorial/ini/monitor.ini + :language: ini + :caption: tutorial/ini/monitor.ini -.. note:: +.. literalinclude:: tutorial/ini/postgres.ini + :language: ini + :caption: tutorial/ini/postgres.ini - The Compose file above uses the imperative ``pg_autoctl create postgres - --run`` form to keep the tutorial self-contained. For production - container deployments the recommended approach is :ref:`pg_autoctl_node_run`: - each node is described in a ``pg_autoctl_node.ini`` file and the command - ``pg_autoctl node run`` handles both creation and startup in one step. - See :ref:`pg_autoctl_node` for the full reference. +The monitor file sets ``kind = monitor``; it has no ``[monitor]`` section +because the monitor is not itself a client of a monitor. The postgres file +is shared by every data node — the node's ``name`` and ``hostname`` are +omitted, so they default to the container hostname set by Docker Compose +(``node1``, ``node2``, ``node3``). -To run the full Citus cluster with HA from this definition, we can use the -following command: +The docker compose definition that assembles these pieces is: -:: +.. literalinclude:: tutorial/docker-compose.yml + :language: yaml + :linenos: - $ docker compose up app monitor node1 node2 +Every service runs the same entry-point — ``pg_autoctl node run`` — with a +different ini file bind-mounted at ``/etc/pgaf/node.ini``. On first start +the command creates the Postgres cluster and registers the node; on +subsequent starts it picks up the existing cluster and runs the supervisor. +Either way the container never needs updating to change node configuration: +edit the ini file and restart. -The command above starts the services up. The first service is the monitor -and is created with the command ``pg_autoctl create monitor``. The options -for this command are exposed in the environment, and could have been -specified on the command line too: +To start the two-node cluster run: :: - $ pg_autoctl create postgres --ssl-self-signed --auth trust --pg-hba-lan --run + $ docker compose up app monitor node1 node2 -While the Postgres nodes are being provisionned by docker compose, you can -run the following command and have a dynamic dashboard to follow what's -happening. The following command is like ``top`` for pg_auto_failover:: +While the nodes are being provisioned you can run the following command and +have a dynamic dashboard to follow what's happening. The following command +is like ``top`` for pg_auto_failover:: $ docker compose exec monitor pg_autoctl watch @@ -244,18 +249,45 @@ We can see the resulting replication settings with the following command: Editing the replication settings while in production ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -It's then possible to change the production architecture obtained with -playing with the :ref:`architecture_setup` commands. Specifically, try the -following command to change the candidate_priority of the node3 to zero, in -order for it to never be a candidate for failover: +Because node3 uses the same ``postgres.ini`` as node1 and node2, its +``candidate_priority`` starts at ``50``. To make node3 a read-only standby +that is never promoted, edit ``tutorial/ini/postgres.ini`` — or better, +since we want to change only node3, create a dedicated ini file for it: -:: +.. code-block:: ini + + # tutorial/ini/node3.ini (same as postgres.ini, different candidate_priority) + [node] + kind = postgres + port = 5432 + + [postgresql] + pgdata = /var/lib/postgres/pgaf + + [monitor] + pguri = postgresql://autoctl_node@monitor/pg_auto_failover + + [settings] + candidate_priority = 0 + replication_quorum = true + + [options] + ssl = self-signed + auth = trust + pg_hba_lan = true + +Update the ``node3`` service in ``docker-compose.yml`` to mount this file +instead:: - $ docker compose exec node3 pg_autoctl set candidate-priority 0 --name node3 + node3: + <<: *node + hostname: node3 + volumes: + - /var/lib/postgres + - ./ini/node3.ini:/etc/pgaf/node.ini:ro -To see the replication settings for all the nodes, the following command can -be useful, and is described in more details in the :ref:`architecture_setup` -section. +The running supervisor on node3 detects the file change and applies the new +``candidate_priority`` live — no restart required. Verify with: :: @@ -307,12 +339,13 @@ To dispose of the entire tutorial environment, just use the following command: Next steps ---------- -As mentioned in the first section of this tutorial, the way we use -docker compose here is not meant to be production ready. It's useful to -understand and play with a distributed system such as Postgres multiple -nodes system and failovers. +The docker compose setup in this tutorial is a good playground for +understanding pg_auto_failover's behaviour. For production container and +Kubernetes deployments, the same ``pg_autoctl node run`` entry-point scales +directly: store your ini files in a ConfigMap or alongside your Compose +file, bind-mount them, and live reconfiguration handles the rest. -See the command :ref:`pg_autoctl_do_tmux_compose_session` for more details -about how to run a docker compose test environment with docker compose, -including external volumes for each node. +See :ref:`pg_autoctl_node` for the full property reference and mutability +table, and the :ref:`container-and-kubernetes-deployments` section of the +Operations guide for production patterns. diff --git a/docs/tutorial/docker-compose.yml b/docs/tutorial/docker-compose.yml index 3a60594d9..7128120f3 100644 --- a/docs/tutorial/docker-compose.yml +++ b/docs/tutorial/docker-compose.yml @@ -6,16 +6,13 @@ x-node: &node - pg_auto_failover:tutorial volumes: - /var/lib/postgres + - ./ini/postgres.ini:/etc/pgaf/node.ini:ro environment: - PGDATA: /var/lib/postgres/pgaf PGUSER: tutorial PGDATABASE: tutorial - PG_AUTOCTL_HBA_LAN: true - PG_AUTOCTL_AUTH_METHOD: "trust" - PG_AUTOCTL_SSL_SELF_SIGNED: true - PG_AUTOCTL_MONITOR: "postgresql://autoctl_node@monitor/pg_auto_failover" expose: - 5432 + command: pg_autoctl node run services: @@ -36,28 +33,19 @@ services: image: pg_auto_failover:tutorial volumes: - /var/lib/postgres - environment: - PGDATA: /var/lib/postgres/pgaf - PG_AUTOCTL_SSL_SELF_SIGNED: true + - ./ini/monitor.ini:/etc/pgaf/node.ini:ro expose: - 5432 - command: | - pg_autoctl create monitor --auth trust --run + command: pg_autoctl node run node1: <<: *node hostname: node1 - command: | - pg_autoctl create postgres --name node1 --run node2: <<: *node hostname: node2 - command: | - pg_autoctl create postgres --name node2 --run node3: <<: *node hostname: node3 - command: | - pg_autoctl create postgres --name node3 --run diff --git a/docs/tutorial/ini/monitor.ini b/docs/tutorial/ini/monitor.ini new file mode 100644 index 000000000..ca835df78 --- /dev/null +++ b/docs/tutorial/ini/monitor.ini @@ -0,0 +1,10 @@ +[node] +kind = monitor +port = 5432 + +[postgresql] +pgdata = /var/lib/postgres/pgaf + +[options] +ssl = self-signed +auth = trust diff --git a/docs/tutorial/ini/postgres.ini b/docs/tutorial/ini/postgres.ini new file mode 100644 index 000000000..8c54b5ec7 --- /dev/null +++ b/docs/tutorial/ini/postgres.ini @@ -0,0 +1,18 @@ +[node] +kind = postgres +port = 5432 + +[postgresql] +pgdata = /var/lib/postgres/pgaf + +[monitor] +pguri = postgresql://autoctl_node@monitor/pg_auto_failover + +[settings] +candidate_priority = 50 +replication_quorum = true + +[options] +ssl = self-signed +auth = trust +pg_hba_lan = true From c3c81d7ff1cd2844ade9a3131a30d1b7a2d11478 Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Mon, 6 Jul 2026 23:59:04 +0200 Subject: [PATCH 09/68] =?UTF-8?q?docs:=20clarify=20candidate=5Fpriority=20?= =?UTF-8?q?change=20=E2=80=94=20direct=20command=20and=20ini=20file=20path?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Show both approaches side by side: - Direct command (pg_autoctl set candidate-priority): immediate, no restart - Declarative ini file: explain that changing docker-compose.yml volumes requires 'docker compose up -d node3' to recreate the container, that pg_autoctl node run applies the ini diff on startup before exec'ing into the supervisor, and that once the dedicated ini file is mounted any subsequent edits to it are picked up live by the running supervisor. --- docs/tutorial.rst | 44 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 2eb7d7705..f9917c3b5 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -251,12 +251,23 @@ Editing the replication settings while in production Because node3 uses the same ``postgres.ini`` as node1 and node2, its ``candidate_priority`` starts at ``50``. To make node3 a read-only standby -that is never promoted, edit ``tutorial/ini/postgres.ini`` — or better, -since we want to change only node3, create a dedicated ini file for it: +that is never promoted there are two ways to proceed. + +**Direct command** — takes effect immediately on the running cluster:: + + $ docker compose exec node3 pg_autoctl set candidate-priority 0 --name node3 + +This reaches into the running supervisor and updates the setting on the +monitor in one step. It is the fastest option when you need an immediate +change on a live cluster. + +**Declarative ini file** — the persistent, version-controlled option. +Because node3 shares ``postgres.ini`` with the other two nodes, you first +create a dedicated ini file for it: .. code-block:: ini - # tutorial/ini/node3.ini (same as postgres.ini, different candidate_priority) + # tutorial/ini/node3.ini [node] kind = postgres port = 5432 @@ -276,8 +287,8 @@ since we want to change only node3, create a dedicated ini file for it: auth = trust pg_hba_lan = true -Update the ``node3`` service in ``docker-compose.yml`` to mount this file -instead:: +Then update the ``node3`` service in ``docker-compose.yml`` to mount this +file instead of the shared one:: node3: <<: *node @@ -286,8 +297,27 @@ instead:: - /var/lib/postgres - ./ini/node3.ini:/etc/pgaf/node.ini:ro -The running supervisor on node3 detects the file change and applies the new -``candidate_priority`` live — no restart required. Verify with: +Changing the ``volumes:`` list requires recreating the container — Docker +cannot swap a bind mount into a running container. Run:: + + $ docker compose up -d node3 + +Docker Compose stops node3, recreates it with the new mount, and starts it +again. ``pg_autoctl node run`` detects that the Postgres cluster already +exists inside the volume, reads ``node3.ini``, and calls ``pg_autoctl node +apply`` before exec'ing into the supervisor. The apply step calls +``pg_autoctl set node candidate-priority 0``, registering the change on the +monitor. node3 rejoins the formation as a secondary within a few seconds. + +.. note:: + + Once node3 has its own ini file mounted, any further edits to + ``tutorial/ini/node3.ini`` on the host are picked up live by the running + supervisor — Docker bind mounts reflect host-side writes immediately, and + the supervisor's file watcher applies mutable fields (``candidate_priority``, + ``replication_quorum``, ``ssl``) without restarting the container. + +Verify with: :: From 2b3d11b29d5d674a4c758566f3aa6081d63a2b83 Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Tue, 7 Jul 2026 00:04:17 +0200 Subject: [PATCH 10/68] docs: rewrite Citus tutorial to use pg_autoctl node run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace all pg_autoctl create coordinator/worker/monitor commands with the declarative pg_autoctl node run approach: New ini files: citus/ini/monitor.ini — kind = monitor citus/ini/coordinator.ini — kind = coordinator, shared by coord0a/coord0b citus/ini/worker.ini — kind = worker, no group (monitor auto-assigns) citus/ini/worker1.ini — kind = worker, group = 1 citus/ini/worker2.ini — kind = worker, group = 2 citus/ini/worker3.ini — kind = worker, group = 3 Rewritten compose files: docker-compose-scale.yml — three services (monitor/coord/worker), each bind-mounts its ini file; all PG_AUTOCTL_* env vars removed docker-compose.yml — named services with per-group worker ini files; YAML anchors kept for coord and per-group worker templates Updated citus-quickstart.rst: - Show all ini files with literalinclude captions before each compose file - Explain that worker.ini without [formation] group triggers monitor auto-assignment (right for --scale), while workerN.ini with group = N pins the pair to a specific shard group (right for named services) - Explain that pg_autoctl node run uses the container hostname as the node name when name is not set in the ini file - Replace stale pg_autoctl_do_tmux_compose_session reference in Next steps with pointer to pg_autoctl_node and container deployments guide --- docs/citus-quickstart.rst | 117 +++++++++++++++------------- docs/citus/docker-compose-scale.yml | 28 ++++--- docs/citus/docker-compose.yml | 89 +++++++++++---------- docs/citus/ini/coordinator.ini | 14 ++++ docs/citus/ini/monitor.ini | 10 +++ docs/citus/ini/worker.ini | 14 ++++ docs/citus/ini/worker1.ini | 17 ++++ docs/citus/ini/worker2.ini | 17 ++++ docs/citus/ini/worker3.ini | 17 ++++ 9 files changed, 210 insertions(+), 113 deletions(-) create mode 100644 docs/citus/ini/coordinator.ini create mode 100644 docs/citus/ini/monitor.ini create mode 100644 docs/citus/ini/worker.ini create mode 100644 docs/citus/ini/worker1.ini create mode 100644 docs/citus/ini/worker2.ini create mode 100644 docs/citus/ini/worker3.ini diff --git a/docs/citus-quickstart.rst b/docs/citus-quickstart.rst index 738b3fdac..f4b20e20a 100644 --- a/docs/citus-quickstart.rst +++ b/docs/citus-quickstart.rst @@ -50,56 +50,55 @@ or run the docker build command directly: Our first Citus Cluster ----------------------- -To create a cluster we use the following docker compose definition: +Each node in the cluster is described by a ``pg_autoctl_node.ini`` file +bind-mounted into its container. There are three files: -.. literalinclude:: citus/docker-compose-scale.yml - :language: yaml - :emphasize-lines: 5,15,27 - :linenos: +.. literalinclude:: citus/ini/monitor.ini + :language: ini + :caption: citus/ini/monitor.ini -.. note:: +.. literalinclude:: citus/ini/coordinator.ini + :language: ini + :caption: citus/ini/coordinator.ini - The Compose file above uses the imperative ``pg_autoctl create - coordinator/worker --run`` form to keep the tutorial self-contained. For - production container deployments the recommended approach is - :ref:`pg_autoctl_node_run`: each node is described in a - ``pg_autoctl_node.ini`` file (with ``kind = coordinator`` or - ``kind = worker``) and ``pg_autoctl node run`` handles both creation and - startup in one step. See :ref:`pg_autoctl_node` for the full reference. +.. literalinclude:: citus/ini/worker.ini + :language: ini + :caption: citus/ini/worker.ini -To run the full Citus cluster with HA from this definition, we can use the -following command: +The ``worker.ini`` has no ``[formation] group`` entry. When that field is +absent the monitor assigns each worker to a group automatically — the first +worker to register in a group becomes primary, the second becomes secondary. +This is the right setup for the scaled deploy below, where we ask Docker +Compose to start six worker containers and let the monitor pair them. -:: +The docker compose definition for the scalable cluster is: - $ docker compose up --scale coord=2 --scale worker=6 - -The command above starts the services up. The command also specifies a -``--scale`` option that is different for each service. We need: +.. literalinclude:: citus/docker-compose-scale.yml + :language: yaml + :linenos: - - one monitor node, and the default scale for a service is 1, +Every service runs ``pg_autoctl node run`` — creating the node on first +start, resuming on subsequent starts. All ``PG_AUTOCTL_*`` environment +variables are gone; everything lives in the ini files. - - one primary Citus coordinator node and one secondary Cituscoordinator - node, which is to say two coordinator nodes, +To run the full Citus cluster with HA from this definition: - - and three Citus worker nodes, each worker with both a primary Postgres - node and a secondary Postgres node, so that's a scale of 6 here. +:: -The default policy for the pg_auto_failover monitor is to assign a primary -and a secondary per auto failover :ref:`group`. In our case, every node -being provisioned with the same command, we benefit from that default policy:: + $ docker compose up --scale coord=2 --scale worker=6 - $ pg_autoctl create worker --ssl-self-signed --auth trust --pg-hba-lan --run +The ``--scale`` options tell Docker Compose how many containers to start for +each service: -When provisioning a production cluster, it is often required to have a -better control over which node participates in which group, then using the -``--group N`` option in the ``pg_autoctl create worker`` command line. + - one monitor node (default scale is 1), + - two coordinator containers — one primary, one secondary, + - six worker containers — the monitor pairs them into three groups of two, + assigning a primary and secondary in each group. -Within a given group, the first node that registers is a primary, and the -other nodes are secondary nodes. The monitor takes care of that in a way -that we don't have to. In a High Availability setup, every node should be -ready to be promoted primary at any time, so knowing which node in a group -is assigned primary first is not very interesting. +Within a given group the first node that registers becomes primary; the +monitor handles the assignment so we don't have to track it. In a High +Availability setup every node must be ready for promotion at any time, so +the initial primary assignment within a group is not significant. While the cluster is being provisionned by docker compose, you can run the following command and have a dynamic dashboard to follow what's happening. @@ -187,26 +186,36 @@ more complex docker compose file than in the previous section. pg_auto_failover architecture with a Citus formation -This time we create a cluster using the following docker compose definition: +This time we need per-group worker ini files so that each worker pair +lands in the right Citus shard group: + +.. literalinclude:: citus/ini/worker1.ini + :language: ini + :caption: citus/ini/worker1.ini (worker1a and worker1b) + +Worker 2 and worker 3 are identical except for ``group = 2`` and +``group = 3`` respectively. When ``group`` is set, ``pg_autoctl node run`` +passes ``--group N`` to ``pg_autoctl create worker``, pinning the pair to +that shard group. + +The docker compose definition is: .. literalinclude:: citus/docker-compose.yml :language: yaml - :emphasize-lines: 3,15,40,44,48,52,56,60,64,68 :linenos: -This definition is a little more involved than the previous one. We take -benefit from `YAML anchors and aliases`__ to define a *template* for our -coordinator nodes and worker nodes, and then apply that template to the -actual nodes. +We use `YAML anchors and aliases`__ to define templates for the coordinator +and each worker group, then apply them to the named services. Each service +sets its own ``hostname:`` — ``pg_autoctl node run`` uses the container +hostname as the node name when ``name`` is not set in the ini file. __ https://yaml101.com/anchors-and-aliases/ -Also this time we provision an application service (named "app") that sits -in the background and allow us to later connect to our current primary -coordinator. See :download:`Dockerfile.app ` for the -complete definition of this service. +Also this time we provision an application service (``app``) that sits in +the background and allows us to connect to the current primary coordinator. +See :download:`Dockerfile.app ` for its definition. -We start this cluster with a simplified command line this time: +We start this cluster with: :: @@ -484,13 +493,15 @@ makes it simple to introduce faults and see how the pg_auto_failover High Availability reacts to those faults. One obvious missing element to better test the system is the lack of -persistent volumes in our docker compose based test rig. It is possible to +persistent volumes in our docker compose based test rig. It is possible to create external volumes and use them for each node in the docker compose -definition. This allows restarting nodes over the same data set. +definition, allowing nodes to restart over the same data set. -See the command :ref:`pg_autoctl_do_tmux_compose_session` for more details -about how to run a docker compose test environment with docker compose, -including external volumes for each node. +For production Kubernetes deployments, the same ini files work as +ConfigMaps: bind-mount them alongside a persistent volume claim for +``/tmp/pgaf`` and use ``pg_autoctl node run`` as the container command. +See :ref:`pg_autoctl_node` for the full property reference and +:ref:`container-and-kubernetes-deployments` for production patterns. Now is a good time to go read `Citus Documentation`__ too, so that you know how to use this cluster you just created! diff --git a/docs/citus/docker-compose-scale.yml b/docs/citus/docker-compose-scale.yml index 08648c236..7184c0e67 100644 --- a/docs/citus/docker-compose-scale.yml +++ b/docs/citus/docker-compose-scale.yml @@ -1,36 +1,34 @@ -version: "3.9" # optional since v1.27.0 - services: monitor: image: pg_auto_failover:citus - environment: - PGDATA: /tmp/pgaf - command: | - pg_autoctl create monitor --ssl-self-signed --auth trust --run + volumes: + - /tmp/pgaf + - ./ini/monitor.ini:/etc/pgaf/node.ini:ro expose: - 5432 + command: pg_autoctl node run coord: image: pg_auto_failover:citus + volumes: + - /tmp/pgaf + - ./ini/coordinator.ini:/etc/pgaf/node.ini:ro environment: - PGDATA: /tmp/pgaf PGUSER: citus PGDATABASE: citus - PG_AUTOCTL_MONITOR: "postgresql://autoctl_node@monitor/pg_auto_failover" expose: - 5432 - command: | - pg_autoctl create coordinator --ssl-self-signed --auth trust --pg-hba-lan --run + command: pg_autoctl node run worker: image: pg_auto_failover:citus + volumes: + - /tmp/pgaf + - ./ini/worker.ini:/etc/pgaf/node.ini:ro environment: - PGDATA: /tmp/pgaf PGUSER: citus PGDATABASE: citus - PG_AUTOCTL_MONITOR: "postgresql://autoctl_node@monitor/pg_auto_failover" expose: - - 5432 - command: | - pg_autoctl create worker --ssl-self-signed --auth trust --pg-hba-lan --run + - 5432 + command: pg_autoctl node run diff --git a/docs/citus/docker-compose.yml b/docs/citus/docker-compose.yml index e56216d62..02aaed8f5 100644 --- a/docs/citus/docker-compose.yml +++ b/docs/citus/docker-compose.yml @@ -1,28 +1,14 @@ x-coord: &coordinator image: pg_auto_failover:citus + volumes: + - /tmp/pgaf + - ./ini/coordinator.ini:/etc/pgaf/node.ini:ro environment: - PGDATA: /tmp/pgaf PGUSER: citus PGDATABASE: citus - PG_AUTOCTL_HBA_LAN: true - PG_AUTOCTL_AUTH_METHOD: "trust" - PG_AUTOCTL_SSL_SELF_SIGNED: true - PG_AUTOCTL_MONITOR: "postgresql://autoctl_node@monitor/pg_auto_failover" - expose: - - 5432 - -x-worker: &worker - image: pg_auto_failover:citus - environment: - PGDATA: /tmp/pgaf - PGUSER: citus - PGDATABASE: citus - PG_AUTOCTL_HBA_LAN: true - PG_AUTOCTL_AUTH_METHOD: "trust" - PG_AUTOCTL_SSL_SELF_SIGNED: true - PG_AUTOCTL_MONITOR: "postgresql://autoctl_node@monitor/pg_auto_failover" expose: - 5432 + command: pg_autoctl node run services: app: @@ -40,58 +26,71 @@ services: monitor: image: pg_auto_failover:citus - environment: - PGDATA: /tmp/pgaf - PG_AUTOCTL_SSL_SELF_SIGNED: true + volumes: + - /tmp/pgaf + - ./ini/monitor.ini:/etc/pgaf/node.ini:ro expose: - 5432 - command: | - pg_autoctl create monitor --auth trust --run + command: pg_autoctl node run coord0a: <<: *coordinator hostname: coord0a - command: | - pg_autoctl create coordinator --name coord0a --run coord0b: <<: *coordinator hostname: coord0b - command: | - pg_autoctl create coordinator --name coord0b --run worker1a: - <<: *worker + <<: &worker1 + image: pg_auto_failover:citus + volumes: + - /tmp/pgaf + - ./ini/worker1.ini:/etc/pgaf/node.ini:ro + environment: + PGUSER: citus + PGDATABASE: citus + expose: + - 5432 + command: pg_autoctl node run hostname: worker1a - command: | - pg_autoctl create worker --group 1 --name worker1a --run worker1b: - <<: *worker + <<: *worker1 hostname: worker1b - command: | - pg_autoctl create worker --group 1 --name worker1b --run worker2a: - <<: *worker + <<: &worker2 + image: pg_auto_failover:citus + volumes: + - /tmp/pgaf + - ./ini/worker2.ini:/etc/pgaf/node.ini:ro + environment: + PGUSER: citus + PGDATABASE: citus + expose: + - 5432 + command: pg_autoctl node run hostname: worker2a - command: | - pg_autoctl create worker --group 2 --name worker2a --run worker2b: - <<: *worker + <<: *worker2 hostname: worker2b - command: | - pg_autoctl create worker --group 2 --name worker2b --run worker3a: - <<: *worker + <<: &worker3 + image: pg_auto_failover:citus + volumes: + - /tmp/pgaf + - ./ini/worker3.ini:/etc/pgaf/node.ini:ro + environment: + PGUSER: citus + PGDATABASE: citus + expose: + - 5432 + command: pg_autoctl node run hostname: worker3a - command: | - pg_autoctl create worker --group 3 --name worker3a --run worker3b: - <<: *worker + <<: *worker3 hostname: worker3b - command: | - pg_autoctl create worker --group 3 --name worker3b --run diff --git a/docs/citus/ini/coordinator.ini b/docs/citus/ini/coordinator.ini new file mode 100644 index 000000000..3f84f7d86 --- /dev/null +++ b/docs/citus/ini/coordinator.ini @@ -0,0 +1,14 @@ +[node] +kind = coordinator +port = 5432 + +[postgresql] +pgdata = /tmp/pgaf + +[monitor] +pguri = postgresql://autoctl_node@monitor/pg_auto_failover + +[options] +ssl = self-signed +auth = trust +pg_hba_lan = true diff --git a/docs/citus/ini/monitor.ini b/docs/citus/ini/monitor.ini new file mode 100644 index 000000000..2aa88a73e --- /dev/null +++ b/docs/citus/ini/monitor.ini @@ -0,0 +1,10 @@ +[node] +kind = monitor +port = 5432 + +[postgresql] +pgdata = /tmp/pgaf + +[options] +ssl = self-signed +auth = trust diff --git a/docs/citus/ini/worker.ini b/docs/citus/ini/worker.ini new file mode 100644 index 000000000..677b3a6c9 --- /dev/null +++ b/docs/citus/ini/worker.ini @@ -0,0 +1,14 @@ +[node] +kind = worker +port = 5432 + +[postgresql] +pgdata = /tmp/pgaf + +[monitor] +pguri = postgresql://autoctl_node@monitor/pg_auto_failover + +[options] +ssl = self-signed +auth = trust +pg_hba_lan = true diff --git a/docs/citus/ini/worker1.ini b/docs/citus/ini/worker1.ini new file mode 100644 index 000000000..ad34e11eb --- /dev/null +++ b/docs/citus/ini/worker1.ini @@ -0,0 +1,17 @@ +[node] +kind = worker +port = 5432 + +[postgresql] +pgdata = /tmp/pgaf + +[monitor] +pguri = postgresql://autoctl_node@monitor/pg_auto_failover + +[formation] +group = 1 + +[options] +ssl = self-signed +auth = trust +pg_hba_lan = true diff --git a/docs/citus/ini/worker2.ini b/docs/citus/ini/worker2.ini new file mode 100644 index 000000000..798fe7884 --- /dev/null +++ b/docs/citus/ini/worker2.ini @@ -0,0 +1,17 @@ +[node] +kind = worker +port = 5432 + +[postgresql] +pgdata = /tmp/pgaf + +[monitor] +pguri = postgresql://autoctl_node@monitor/pg_auto_failover + +[formation] +group = 2 + +[options] +ssl = self-signed +auth = trust +pg_hba_lan = true diff --git a/docs/citus/ini/worker3.ini b/docs/citus/ini/worker3.ini new file mode 100644 index 000000000..6cecbdeb8 --- /dev/null +++ b/docs/citus/ini/worker3.ini @@ -0,0 +1,17 @@ +[node] +kind = worker +port = 5432 + +[postgresql] +pgdata = /tmp/pgaf + +[monitor] +pguri = postgresql://autoctl_node@monitor/pg_auto_failover + +[formation] +group = 3 + +[options] +ssl = self-signed +auth = trust +pg_hba_lan = true From d2d4684b1e362b83c31481a7f00702243a6d9ce9 Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Tue, 7 Jul 2026 00:14:16 +0200 Subject: [PATCH 11/68] style: apply citus_indent to nodespec.c --- src/bin/pg_autoctl/nodespec.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/bin/pg_autoctl/nodespec.c b/src/bin/pg_autoctl/nodespec.c index 2a30c46dc..cad52241c 100644 --- a/src/bin/pg_autoctl/nodespec.c +++ b/src/bin/pg_autoctl/nodespec.c @@ -115,7 +115,7 @@ nodespec_read(const char *path, NodeSpec *spec) replicationQuorumStr, "true"), /* [options] — ssl is mutable (applied via `pg_autoctl enable ssl`); - auth and pg_hba_lan are create-time only */ + * auth and pg_hba_lan are create-time only */ make_strbuf_option_default("options", "ssl", NULL, false, sizeof(spec->ssl), spec->ssl, "self-signed"), @@ -458,11 +458,11 @@ nodespec_create_argv(const NodeSpec *spec, int i = 0; #define PUSH(v) do { \ - if (i >= args_size - 1) { \ - log_error("nodespec_create_argv: args[] overflow"); \ - return -1; \ - } \ - args[i++] = (char *) (v); \ + if (i >= args_size - 1) { \ + log_error("nodespec_create_argv: args[] overflow"); \ + return -1; \ + } \ + args[i++] = (char *) (v); \ } while (0) PUSH(pg_autoctl_path); From c57cf3b583e7275592f8cfafda708a9c24742391 Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Tue, 7 Jul 2026 00:15:55 +0200 Subject: [PATCH 12/68] style: fix nodespec.c PUSH macro indentation per citus_indent --- src/bin/pg_autoctl/nodespec.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/bin/pg_autoctl/nodespec.c b/src/bin/pg_autoctl/nodespec.c index cad52241c..5f61e6d91 100644 --- a/src/bin/pg_autoctl/nodespec.c +++ b/src/bin/pg_autoctl/nodespec.c @@ -458,11 +458,11 @@ nodespec_create_argv(const NodeSpec *spec, int i = 0; #define PUSH(v) do { \ - if (i >= args_size - 1) { \ - log_error("nodespec_create_argv: args[] overflow"); \ - return -1; \ - } \ - args[i++] = (char *) (v); \ + if (i >= args_size - 1) { \ + log_error("nodespec_create_argv: args[] overflow"); \ + return -1; \ + } \ + args[i++] = (char *) (v); \ } while (0) PUSH(pg_autoctl_path); From bb5e09aea7818a8e4210e0bdcd127db269e0a060 Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Tue, 7 Jul 2026 00:26:53 +0200 Subject: [PATCH 13/68] feat: add src/bin/common/ shared library, pgaftest binary and .pgaf spec suite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Combines what would have been two separate PRs: - src/bin/common/: move ~20 utility sources (pgsql, pgsetup, ini_file, …) out of pg_autoctl/ into a shared static library linked by both binaries - src/bin/pgaftest/: new test binary — reads .pgaf specs, runs headless CI (TAP output) or interactive setup with docker compose - tests/tap/specs/: 35 .pgaf spec files covering the full test matrix - tests/tap/schedule: ordered run list for CI - tests/upgrade/: dual-binary upgrade test image and shim script - .github/workflows/run-pgaftest.yml: CI workflow — builds pgaf:run image and pgaftest binary once, fans out one job per spec in parallel - Dockerfile: add pgaftest build/install target, bison touch guard - style: all new C files reformatted with citus/stylechecker:no-py Docker image --- .github/workflows/run-pgaftest.yml | 259 ++ .gitignore | 3 + Dockerfile | 118 +- src/bin/Makefile | 17 +- src/bin/common/Makefile | 16 + src/bin/common/Makefile.common | 129 + src/bin/common/debian.c | 717 +++ src/bin/common/debian.h | 68 + src/bin/common/env_utils.c | 166 + src/bin/common/env_utils.h | 22 + src/bin/common/file_utils.c | 943 ++++ src/bin/common/file_utils.h | 73 + src/bin/common/ini_file.c | 743 +++ src/bin/common/ini_file.h | 117 + src/bin/common/ini_implementation.c | 13 + src/bin/common/ipaddr.c | 922 ++++ src/bin/common/ipaddr.h | 40 + src/bin/common/lock_utils.c | 382 ++ src/bin/common/lock_utils.h | 40 + src/bin/common/parsing.c | 1221 +++++ src/bin/common/parsing.h | 100 + src/bin/common/pgctl.c | 2854 ++++++++++++ src/bin/common/pgctl.h | 79 + src/bin/common/pgsetup.c | 2049 +++++++++ src/bin/common/pgsetup.h | 282 ++ src/bin/common/pgsql.c | 3470 ++++++++++++++ src/bin/common/pgsql.h | 399 ++ src/bin/common/pgtuning.c | 390 ++ src/bin/common/pgtuning.h | 19 + src/bin/common/pidfile.c | 568 +++ src/bin/common/pidfile.h | 74 + src/bin/common/signals.c | 260 ++ src/bin/common/signals.h | 38 + src/bin/common/string_utils.c | 601 +++ src/bin/common/string_utils.h | 44 + src/bin/common/system_utils.c | 145 + src/bin/common/system_utils.h | 27 + src/bin/pg_autoctl/Makefile | 97 +- src/bin/pgaftest/Makefile | 120 + src/bin/pgaftest/cli_demo.c | 69 + src/bin/pgaftest/cli_demo.h | 16 + src/bin/pgaftest/cli_indent.c | 1149 +++++ src/bin/pgaftest/cli_indent.h | 15 + src/bin/pgaftest/cli_root.c | 575 +++ src/bin/pgaftest/compose_gen.c | 1252 +++++ src/bin/pgaftest/compose_gen.h | 70 + src/bin/pgaftest/main.c | 54 + src/bin/pgaftest/pgaftest | Bin 0 -> 870520 bytes src/bin/pgaftest/test_runner.c | 3806 +++++++++++++++ src/bin/pgaftest/test_runner.h | 100 + src/bin/pgaftest/test_spec.h | 256 ++ src/bin/pgaftest/test_spec_parse.c | 4074 +++++++++++++++++ src/bin/pgaftest/test_spec_parse.h | 278 ++ src/bin/pgaftest/test_spec_parse.y | 1489 ++++++ src/bin/pgaftest/test_spec_scan.c | 3912 ++++++++++++++++ src/bin/pgaftest/test_spec_scan.l | 390 ++ tests/tap/README.md | 217 + tests/tap/schedule | 36 + tests/tap/specs/auth.pgaf | 76 + tests/tap/specs/basic_citus_operation.pgaf | 132 + tests/tap/specs/basic_operation.pgaf | 402 ++ .../specs/basic_operation_listen_flag.pgaf | 117 + tests/tap/specs/citus_cluster_name.pgaf | 120 + tests/tap/specs/citus_force_failover.pgaf | 86 + tests/tap/specs/citus_multi_standbys.pgaf | 176 + tests/tap/specs/citus_skip_pg_hba.pgaf | 124 + tests/tap/specs/config_get_set.pgaf | 48 + .../tap/specs/create_standby_with_pgdata.pgaf | 80 + tests/tap/specs/debian_clusters.pgaf | 51 + tests/tap/specs/enable_ssl.pgaf | 84 + tests/tap/specs/ensure.pgaf | 69 + tests/tap/specs/extension_update.pgaf | 37 + tests/tap/specs/installcheck.pgaf | 46 + tests/tap/specs/maintenance_and_drop.pgaf | 75 + tests/tap/specs/monitor_disabled.pgaf | 98 + tests/tap/specs/multi_alternate.pgaf | 267 ++ tests/tap/specs/multi_async.pgaf | 251 + tests/tap/specs/multi_ifdown.pgaf | 178 + tests/tap/specs/multi_maintenance.pgaf | 278 ++ tests/tap/specs/multi_standbys.pgaf | 303 ++ tests/tap/specs/nonha_citus_operation.pgaf | 104 + tests/tap/specs/replace_monitor.pgaf | 95 + tests/tap/specs/skip_pg_hba.pgaf | 58 + tests/tap/specs/ssl_cert.pgaf | 85 + tests/tap/specs/ssl_self_signed.pgaf | 76 + tests/tap/specs/tablespaces.pgaf | 148 + tests/tap/specs/upgrade.pgaf | 189 + tests/upgrade/Dockerfile.current | 81 + tests/upgrade/Makefile | 158 +- tests/upgrade/install-extension.sh | 14 + tests/upgrade/pg_autoctl_shim.sh | 95 + 91 files changed, 39441 insertions(+), 143 deletions(-) create mode 100644 .github/workflows/run-pgaftest.yml create mode 100644 src/bin/common/Makefile create mode 100644 src/bin/common/Makefile.common create mode 100644 src/bin/common/debian.c create mode 100644 src/bin/common/debian.h create mode 100644 src/bin/common/env_utils.c create mode 100644 src/bin/common/env_utils.h create mode 100644 src/bin/common/file_utils.c create mode 100644 src/bin/common/file_utils.h create mode 100644 src/bin/common/ini_file.c create mode 100644 src/bin/common/ini_file.h create mode 100644 src/bin/common/ini_implementation.c create mode 100644 src/bin/common/ipaddr.c create mode 100644 src/bin/common/ipaddr.h create mode 100644 src/bin/common/lock_utils.c create mode 100644 src/bin/common/lock_utils.h create mode 100644 src/bin/common/parsing.c create mode 100644 src/bin/common/parsing.h create mode 100644 src/bin/common/pgctl.c create mode 100644 src/bin/common/pgctl.h create mode 100644 src/bin/common/pgsetup.c create mode 100644 src/bin/common/pgsetup.h create mode 100644 src/bin/common/pgsql.c create mode 100644 src/bin/common/pgsql.h create mode 100644 src/bin/common/pgtuning.c create mode 100644 src/bin/common/pgtuning.h create mode 100644 src/bin/common/pidfile.c create mode 100644 src/bin/common/pidfile.h create mode 100644 src/bin/common/signals.c create mode 100644 src/bin/common/signals.h create mode 100644 src/bin/common/string_utils.c create mode 100644 src/bin/common/string_utils.h create mode 100644 src/bin/common/system_utils.c create mode 100644 src/bin/common/system_utils.h create mode 100644 src/bin/pgaftest/Makefile create mode 100644 src/bin/pgaftest/cli_demo.c create mode 100644 src/bin/pgaftest/cli_demo.h create mode 100644 src/bin/pgaftest/cli_indent.c create mode 100644 src/bin/pgaftest/cli_indent.h create mode 100644 src/bin/pgaftest/cli_root.c create mode 100644 src/bin/pgaftest/compose_gen.c create mode 100644 src/bin/pgaftest/compose_gen.h create mode 100644 src/bin/pgaftest/main.c create mode 100755 src/bin/pgaftest/pgaftest create mode 100644 src/bin/pgaftest/test_runner.c create mode 100644 src/bin/pgaftest/test_runner.h create mode 100644 src/bin/pgaftest/test_spec.h create mode 100644 src/bin/pgaftest/test_spec_parse.c create mode 100644 src/bin/pgaftest/test_spec_parse.h create mode 100644 src/bin/pgaftest/test_spec_parse.y create mode 100644 src/bin/pgaftest/test_spec_scan.c create mode 100644 src/bin/pgaftest/test_spec_scan.l create mode 100644 tests/tap/README.md create mode 100644 tests/tap/schedule create mode 100644 tests/tap/specs/auth.pgaf create mode 100644 tests/tap/specs/basic_citus_operation.pgaf create mode 100644 tests/tap/specs/basic_operation.pgaf create mode 100644 tests/tap/specs/basic_operation_listen_flag.pgaf create mode 100644 tests/tap/specs/citus_cluster_name.pgaf create mode 100644 tests/tap/specs/citus_force_failover.pgaf create mode 100644 tests/tap/specs/citus_multi_standbys.pgaf create mode 100644 tests/tap/specs/citus_skip_pg_hba.pgaf create mode 100644 tests/tap/specs/config_get_set.pgaf create mode 100644 tests/tap/specs/create_standby_with_pgdata.pgaf create mode 100644 tests/tap/specs/debian_clusters.pgaf create mode 100644 tests/tap/specs/enable_ssl.pgaf create mode 100644 tests/tap/specs/ensure.pgaf create mode 100644 tests/tap/specs/extension_update.pgaf create mode 100644 tests/tap/specs/installcheck.pgaf create mode 100644 tests/tap/specs/maintenance_and_drop.pgaf create mode 100644 tests/tap/specs/monitor_disabled.pgaf create mode 100644 tests/tap/specs/multi_alternate.pgaf create mode 100644 tests/tap/specs/multi_async.pgaf create mode 100644 tests/tap/specs/multi_ifdown.pgaf create mode 100644 tests/tap/specs/multi_maintenance.pgaf create mode 100644 tests/tap/specs/multi_standbys.pgaf create mode 100644 tests/tap/specs/nonha_citus_operation.pgaf create mode 100644 tests/tap/specs/replace_monitor.pgaf create mode 100644 tests/tap/specs/skip_pg_hba.pgaf create mode 100644 tests/tap/specs/ssl_cert.pgaf create mode 100644 tests/tap/specs/ssl_self_signed.pgaf create mode 100644 tests/tap/specs/tablespaces.pgaf create mode 100644 tests/tap/specs/upgrade.pgaf create mode 100644 tests/upgrade/Dockerfile.current create mode 100644 tests/upgrade/install-extension.sh create mode 100644 tests/upgrade/pg_autoctl_shim.sh diff --git a/.github/workflows/run-pgaftest.yml b/.github/workflows/run-pgaftest.yml new file mode 100644 index 000000000..0f0be634d --- /dev/null +++ b/.github/workflows/run-pgaftest.yml @@ -0,0 +1,259 @@ +name: pgaftest + +on: + push: + branches: + - main + pull_request: + branches: + - main + workflow_dispatch: + +# One run per branch; cancel in-progress runs on new push. +concurrency: + group: pgaftest-${{ github.ref }} + cancel-in-progress: true + +jobs: + # --------------------------------------------------------------------------- + # Build the runtime Docker image and the pgaftest binary once, then share + # both as artifacts with every parallel test job. + # --------------------------------------------------------------------------- + build: + name: Build image & pgaftest + runs-on: ubuntu-latest + env: + PGVERSION: 17 + + steps: + - uses: actions/checkout@v4.2.2 + + # git-version.h is generated from the working tree and must exist on the + # host before the Docker build COPY step reads it. + - name: Generate git-version.h + run: make version + + - name: Build pg_auto_failover run image + run: | + docker build \ + --build-arg PGVERSION=${{ env.PGVERSION }} \ + --target run \ + -t pgaf:run \ + . + + - name: Save pgaf:run image as a tarball + run: docker save pgaf:run | gzip > /tmp/pgaf-run.tar.gz + + - name: Upload pgaf:run image artifact + uses: actions/upload-artifact@v4.4.3 + with: + name: pgaf-run-image + path: /tmp/pgaf-run.tar.gz + # Only needed for the duration of this workflow run. + retention-days: 1 + + # Build the full build image just to extract the pgaftest binary. + # The binary is linux/amd64, linked against PGDG libpq — matches + # ubuntu-latest runners where the test jobs will execute it. + - name: Build pg_auto_failover build image (for pgaftest extraction) + run: | + docker build \ + --build-arg PGVERSION=${{ env.PGVERSION }} \ + --target build \ + -t pgaf:build \ + . + + - name: Extract pgaftest binary from build image + run: | + docker create --name pgaftest-extract pgaf:build + docker cp pgaftest-extract:/usr/lib/postgresql/${{ env.PGVERSION }}/bin/pgaftest \ + /tmp/pgaftest-bin + docker rm pgaftest-extract + chmod +x /tmp/pgaftest-bin + + - name: Upload pgaftest binary artifact + uses: actions/upload-artifact@v4.4.3 + with: + name: pgaftest-binary + path: /tmp/pgaftest-bin + retention-days: 1 + + # --------------------------------------------------------------------------- + # Run all non-upgrade specs from tests/tap/schedule in a matrix. + # Each job is independent; they all share the pre-built image and binary. + # --------------------------------------------------------------------------- + test: + name: pgaftest / ${{ matrix.spec }} + needs: build + runs-on: ubuntu-latest + timeout-minutes: 25 + strategy: + fail-fast: false + matrix: + spec: + - basic_operation + - basic_operation_listen_flag + - maintenance_and_drop + - create_standby_with_pgdata + - ensure + - monitor_disabled + - replace_monitor + - config_get_set + - skip_pg_hba + - auth + - enable_ssl + - ssl_cert + - ssl_self_signed + - multi_standbys + - multi_async + - multi_ifdown + - multi_maintenance + - multi_alternate + - extension_update + + steps: + - uses: actions/checkout@v4.2.2 + + - name: Download pgaf:run image + uses: actions/download-artifact@v4.1.8 + with: + name: pgaf-run-image + path: /tmp + + - name: Load pgaf:run image into Docker + run: docker load < /tmp/pgaf-run.tar.gz + + - name: Download pgaftest binary + uses: actions/download-artifact@v4.1.8 + with: + name: pgaftest-binary + path: /tmp + + # pgaftest links against PGDG libpq — install the matching runtime lib. + - name: Install libpq runtime + run: | + sudo apt-get install -y curl ca-certificates gnupg + curl https://www.postgresql.org/media/keys/ACCC4CF8.asc \ + | sudo apt-key add - + echo "deb http://apt.postgresql.org/pub/repos/apt \ + $(lsb_release -cs)-pgdg main" \ + | sudo tee /etc/apt/sources.list.d/pgdg.list + sudo apt-get update + sudo apt-get install -y libpq5 + + - name: Run ${{ matrix.spec }} + timeout-minutes: 18 + env: + PGAF_IMAGE: pgaf:run + run: | + chmod +x /tmp/pgaftest-bin + /tmp/pgaftest-bin run tests/tap/specs/${{ matrix.spec }}.pgaf + + # --------------------------------------------------------------------------- + # installcheck: builds pgaf:testrun (run + source tree + postgresql-server-dev) + # and runs the SQL regression test suite via pg_regress. Kept separate from + # the main matrix because it requires a larger image built inline. + # --------------------------------------------------------------------------- + installcheck: + name: pgaftest / installcheck + needs: build + runs-on: ubuntu-latest + timeout-minutes: 20 + env: + PGVERSION: 17 + + steps: + - uses: actions/checkout@v4.2.2 + + - name: Install libpq runtime + run: | + sudo apt-get install -y curl ca-certificates gnupg + curl https://www.postgresql.org/media/keys/ACCC4CF8.asc \ + | sudo apt-key add - + echo "deb http://apt.postgresql.org/pub/repos/apt \ + $(lsb_release -cs)-pgdg main" \ + | sudo tee /etc/apt/sources.list.d/pgdg.list + sudo apt-get update + sudo apt-get install -y libpq5 + + - name: Download pgaftest binary + uses: actions/download-artifact@v4.1.8 + with: + name: pgaftest-binary + path: /tmp + + - name: Generate git-version.h + run: make version + + - name: Build pgaf:testrun image + run: | + docker build \ + --build-arg PGVERSION=${{ env.PGVERSION }} \ + --target testrun \ + -t pgaf:testrun \ + . + + - name: Run installcheck spec + timeout-minutes: 15 + run: | + chmod +x /tmp/pgaftest-bin + /tmp/pgaftest-bin run tests/tap/specs/installcheck.pgaf + + # --------------------------------------------------------------------------- + # Upgrade test: builds both pgaf:next (current branch) and pgaf:current + # (the previous release tag, auto-detected), then runs the upgrade spec. + # Kept separate because it needs a PGVERSION=16 image and additional + # build steps (pgaf:current-base from git archive of PREV_TAG). + # --------------------------------------------------------------------------- + upgrade: + name: pgaftest / upgrade + needs: build + runs-on: ubuntu-latest + timeout-minutes: 30 + env: + PGVERSION: 16 + + steps: + - uses: actions/checkout@v4.2.2 + with: + # Full history needed so PREV_TAG auto-detection (git tag --sort) + # finds the correct previous release tag. + fetch-depth: 0 + + - name: Install libpq runtime + run: | + sudo apt-get install -y curl ca-certificates gnupg + curl https://www.postgresql.org/media/keys/ACCC4CF8.asc \ + | sudo apt-key add - + echo "deb http://apt.postgresql.org/pub/repos/apt \ + $(lsb_release -cs)-pgdg main" \ + | sudo tee /etc/apt/sources.list.d/pgdg.list + sudo apt-get update + sudo apt-get install -y libpq5 + + - name: Download pgaftest binary + uses: actions/download-artifact@v4.1.8 + with: + name: pgaftest-binary + path: /tmp + + - name: Generate git-version.h + run: make version + + # The build job already produced pgaf:run at PG16 for the regular tests, + # but the upgrade job runs independently and needs its own images. + # Build pgaf:next first so pgaf:current's Dockerfile.current can COPY + # the v2.2 binary and extension files from it. + - name: Build pgaf:next (current branch, PGVERSION=16) + run: | + make -C tests/upgrade pgaf-next PGVERSION=${{ env.PGVERSION }} + + - name: Build pgaf:current (previous release + both binaries baked in) + run: | + make -C tests/upgrade pgaf-current PGVERSION=${{ env.PGVERSION }} + + - name: Run upgrade spec + timeout-minutes: 25 + run: | + chmod +x /tmp/pgaftest-bin + /tmp/pgaftest-bin run tests/tap/specs/upgrade.pgaf diff --git a/.gitignore b/.gitignore index 3d0a5f4fa..94b523b1c 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,6 @@ docs/tikz/*.png # Exclude our demo/test tmux directory tmux/ valgrind/ +*.tmp +src/bin/pgaftest/test_spec_parse.tab.* +src/bin/pgaftest/test_spec_parse.output diff --git a/Dockerfile b/Dockerfile index 1df68994d..091f8134f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -74,7 +74,7 @@ RUN apt-get update \ postgresql-${PGVERSION} \ && rm -rf /var/lib/apt/lists/* -RUN pip3 install pyroute2>=0.5.17 +RUN pip3 install 'pyroute2>=0.5.17,<0.7.0' RUN adduser --disabled-password --gecos '' docker RUN adduser docker sudo @@ -112,6 +112,16 @@ COPY Makefile.azure ./ COPY Makefile.citus ./ COPY ./src/ ./src COPY ./src/bin/pg_autoctl/git-version.h ./src/bin/pg_autoctl/git-version.h +# Touch bison/flex generated files so they appear newer than the grammar +# sources, preventing make from re-running bison (system bison version may +# differ from the one used to pre-generate the committed .c/.h files). +# Guard with -d: older releases (e.g. v2.1 built via git-archive for upgrade +# tests) do not have src/bin/pgaftest/ at all. +RUN if [ -d src/bin/pgaftest ]; then \ + touch src/bin/pgaftest/test_spec_parse.c \ + src/bin/pgaftest/test_spec_parse.h \ + src/bin/pgaftest/test_spec_scan.c; \ + fi RUN make -s clean && make -s install -j8 @@ -196,4 +206,108 @@ ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/lib/p ENV PG_AUTOCTL_DEBUG=1 ENV PGDATA=/var/lib/postgres/pgaf -CMD ["pg_autoctl", "do tmux session --nodes 3 --binpath /usr/local/bin/pg_autoctl"] +# +# debian image — like run, but with a Debian-style "main" cluster pre-created +# via pg_createcluster so that pg_autoctl can test adoption of the split-config +# layout (postgresql.conf lives in /etc/postgresql/${PGVERSION}/main/ outside +# PGDATA). pg_autoctl node run detects the missing postgresql.conf in PGDATA, +# moves the conf files in, and proceeds normally — no entrypoint changes needed. +# +FROM run AS debian + +ARG PGVERSION + +USER root +RUN pg_createcluster \ + --user docker --group postgres \ + ${PGVERSION} main \ + -- --auth-local trust --auth-host trust \ + && chown docker /var/lib/postgresql/${PGVERSION} + +USER docker +ENV PGDATA=/var/lib/postgresql/${PGVERSION}/main + +# +# testrun image — like run, but adds postgresql-server-dev and the full source +# tree so that "make installcheck" works inside the monitor container. +# Used by installcheck.pgaf via "monitor image-target testrun". +# +FROM run AS testrun + +ARG PGVERSION + +USER root +RUN apt-get update \ + && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + make \ + postgresql-server-dev-${PGVERSION} \ + && rm -rf /var/lib/apt/lists/* + +COPY --chown=docker ./src/ /usr/src/pg_auto_failover/src/ + +USER docker + +# +# pgaftest image — standalone test-runner image. +# +# Kept entirely separate from the pg_auto_failover run image so that no +# test tooling leaks into the images used for monitor and data nodes. +# +# Runtime dependencies come from two sources: +# - Docker's own apt repo → docker-ce-cli + docker-compose-plugin (v2) +# - PGDG apt repo → libpq5 matching the build's PGVERSION +# +# The pg_auto_failover binaries are copied directly from the run stage; +# nothing is built here. +# +FROM debian:bullseye-slim AS pgaftest + +ARG PGVERSION + +# Minimal runtime libs (libssl, libcurl, libzstd) plus PGDG for libpq +RUN apt-get update \ + && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + gnupg \ + libcurl4-gnutls-dev \ + libncurses6 \ + libzstd-dev \ + sudo \ + && rm -rf /var/lib/apt/lists/* + +# PGDG repo — needed for the libpq version that pg_autoctl and pgaftest link against +RUN curl https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - +RUN echo "deb http://apt.postgresql.org/pub/repos/apt bullseye-pgdg main ${PGVERSION}" \ + > /etc/apt/sources.list.d/pgdg.list \ + && apt-get update \ + && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + libpq5 \ + && rm -rf /var/lib/apt/lists/* + +# Docker's apt repo — provides docker-ce-cli and the compose v2 plugin +RUN curl -fsSL https://download.docker.com/linux/debian/gpg \ + | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg \ + && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] \ + https://download.docker.com/linux/debian bullseye stable" \ + > /etc/apt/sources.list.d/docker.list \ + && apt-get update \ + && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + docker-ce-cli \ + docker-compose-plugin \ + && rm -rf /var/lib/apt/lists/* + +RUN adduser --disabled-password --gecos '' --home /var/lib/postgres docker \ + && adduser docker sudo \ + && echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers \ + && mkdir -p /var/lib/postgres \ + && chown docker /var/lib/postgres + +# Binaries from the pg_auto_failover run image +COPY --from=run /usr/local/bin/pg_autoctl /usr/local/bin/ + +# pgaftest binary from the build stage +COPY --from=build /usr/lib/postgresql/${PGVERSION}/bin/pgaftest /usr/local/bin/ + +USER docker +ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin diff --git a/src/bin/Makefile b/src/bin/Makefile index 7c743833d..fcdca4ae7 100644 --- a/src/bin/Makefile +++ b/src/bin/Makefile @@ -1,15 +1,24 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the PostgreSQL License. -all: pg_autoctl ; +all: common pg_autoctl pgaftest ; -pg_autoctl: +common: + $(MAKE) -C common + +pg_autoctl: common $(MAKE) -C pg_autoctl pg_autoctl +pgaftest: common + $(MAKE) -C pgaftest pgaftest + clean: + $(MAKE) -C common clean $(MAKE) -C pg_autoctl clean + $(MAKE) -C pgaftest clean -install: $(pg_autoctl) +install: pg_autoctl pgaftest $(MAKE) -C pg_autoctl install + $(MAKE) -C pgaftest install -.PHONY: all pg_autoctl install clean +.PHONY: all common pg_autoctl pgaftest install clean diff --git a/src/bin/common/Makefile b/src/bin/common/Makefile new file mode 100644 index 000000000..5b106fcc4 --- /dev/null +++ b/src/bin/common/Makefile @@ -0,0 +1,16 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the PostgreSQL License. + +SRC_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST)))) + +.DEFAULT_GOAL := $(SRC_DIR)libpgaf_common.a + +include $(SRC_DIR)Makefile.common + +override CFLAGS += -I$(SRC_DIR)../pg_autoctl + +clean: + rm -f $(COMMON_OBJ) $(COMMON_LIB) + rm -rf $(DEPDIR) + +.PHONY: clean diff --git a/src/bin/common/Makefile.common b/src/bin/common/Makefile.common new file mode 100644 index 000000000..be7a0fe9e --- /dev/null +++ b/src/bin/common/Makefile.common @@ -0,0 +1,129 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the PostgreSQL License. +# +# Makefile.common — shared build variables and common source library. +# Included by both pg_autoctl and pgaftest Makefiles via: +# include ../common/Makefile.common + +PG_CONFIG ?= pg_config + +COMMON_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST)))) +LIB_DIR := $(COMMON_DIR)../lib + +PG_SNPRINTF = $(wildcard $(LIB_DIR)/pg/snprintf.*) +LOG_SRC = $(wildcard $(LIB_DIR)/log/src/log.*) +COMMANDLINE_SRC = $(wildcard $(LIB_DIR)/subcommands.c/commandline.*) +PARSON_SRC = $(wildcard $(LIB_DIR)/parson/parson.*) + +COMMON_INCLUDES = -I$(COMMON_DIR) +COMMON_INCLUDES += -I$(LIB_DIR)/pg +COMMON_INCLUDES += -I$(LIB_DIR)/log/src/ +COMMON_INCLUDES += -I$(LIB_DIR)/subcommands.c/ +COMMON_INCLUDES += -I$(LIB_DIR)/libs/ +COMMON_INCLUDES += -I$(LIB_DIR)/parson/ + +CC = $(shell $(PG_CONFIG) --cc) + +DEFAULT_CFLAGS = -std=c99 -D_GNU_SOURCE -g +DEFAULT_CFLAGS += -I $(shell $(PG_CONFIG) --includedir) +DEFAULT_CFLAGS += -I $(shell $(PG_CONFIG) --includedir-server) +DEFAULT_CFLAGS += -I $(shell $(PG_CONFIG) --pkgincludedir)/internal +DEFAULT_CFLAGS += $(shell $(PG_CONFIG) --cflags) +DEFAULT_CFLAGS += -Wformat +DEFAULT_CFLAGS += -Wall +DEFAULT_CFLAGS += -Werror=implicit-int +DEFAULT_CFLAGS += -Werror=implicit-function-declaration +DEFAULT_CFLAGS += -Werror=return-type +DEFAULT_CFLAGS += -Wno-declaration-after-statement +DEFAULT_CFLAGS += -D_WANT_SEMUN +DEFAULT_CFLAGS += -Wno-missing-braces +DEFAULT_CFLAGS += $(COMMON_INCLUDES) + +# On macOS, gettext is keg-only and not in the default include path. +# postgres_fe.h → c.h → libintl.h requires finding it explicitly. +ifeq ($(shell uname -s),Darwin) + GETTEXT_PREFIX := $(shell brew --prefix gettext 2>/dev/null) + ifneq ($(GETTEXT_PREFIX),) + DEFAULT_CFLAGS += -I$(GETTEXT_PREFIX)/include + endif +endif + +override CFLAGS := $(DEFAULT_CFLAGS) $(CFLAGS) + +BINDIR ?= $(shell $(PG_CONFIG) --bindir) + +# pg_config --ldflags from source builds can emit colon-joined paths such as +# -L/path/a:/path/b as a single token, which the linker rejects. Split each +# -L token on ':' so every directory gets its own -L flag. +PG_LDFLAGS_RAW := $(shell $(PG_CONFIG) --ldflags) +PG_LDFLAGS := $(shell echo "$(PG_LDFLAGS_RAW)" | awk '{ \ + for (i = 1; i <= NF; i++) { \ + if (substr($$i, 1, 2) == "-L") { \ + n = split(substr($$i, 3), p, ":"); \ + for (j = 1; j <= n; j++) if (p[j] != "") printf "-L%s ", p[j]; \ + } else { printf "%s ", $$i; } \ + } \ +}') + +LIBS = -L $(shell $(PG_CONFIG) --pkglibdir) +LIBS += -L $(shell $(PG_CONFIG) --libdir) +LIBS += $(PG_LDFLAGS) +LIBS += $(shell $(PG_CONFIG) --libs) +LIBS += -lpq + +DEPDIR = .deps + +# ----------------------------------------------------------------------- +# Common source library (src/bin/common/*.c → libpgaf_common.a) +# Callers link with: $(COMMON_LIB) and add $(COMMON_LIB) to OBJS. +# ----------------------------------------------------------------------- + +COMMON_SRC = $(wildcard $(COMMON_DIR)*.c) +COMMON_OBJ = $(patsubst $(COMMON_DIR)%.c,$(COMMON_DIR)%.o,$(COMMON_SRC)) +COMMON_LIB = $(COMMON_DIR)libpgaf_common.a + +# Compile rule for common objects (run from each caller's directory) +$(COMMON_DIR)%.o: $(COMMON_DIR)%.c + @if test ! -d $(COMMON_DIR)$(DEPDIR); then mkdir -p $(COMMON_DIR)$(DEPDIR); fi + $(CC) $(CFLAGS) -c -MMD -MP -MF$(COMMON_DIR)$(DEPDIR)/$(*F).Po -o $@ $< + +$(COMMON_LIB): $(COMMON_OBJ) + $(AR) rcs $@ $^ + +Po_common_files := $(wildcard $(COMMON_DIR)$(DEPDIR)/*.Po) +ifneq (,$(Po_common_files)) +include $(Po_common_files) +endif + +# ----------------------------------------------------------------------- +# Compile rule for caller's own .c → .o with auto-dependency tracking +# ----------------------------------------------------------------------- +%.o : %.c + @if test ! -d $(DEPDIR); then mkdir -p $(DEPDIR); fi + $(CC) $(CFLAGS) -c -MMD -MP -MF$(DEPDIR)/$(*F).Po -o $@ $< + +Po_files := $(wildcard $(DEPDIR)/*.Po) +ifneq (,$(Po_files)) +include $(Po_files) +endif + +# Shared rules for vendored libs +lib-snprintf.o: $(PG_SNPRINTF) + $(CC) $(CFLAGS) -c -MMD -MP -MF$(DEPDIR)/lib-snprintf.Po -MT$@ \ + -o $@ $(LIB_DIR)/pg/snprintf.c + +lib-strerror.o: $(PG_SNPRINTF) + $(CC) $(CFLAGS) -c -MMD -MP -MF$(DEPDIR)/lib-strerror.Po -MT$@ \ + -o $@ $(LIB_DIR)/pg/strerror.c + +lib-log.o: $(LOG_SRC) + $(CC) $(CFLAGS) -c -MMD -MP -MF$(DEPDIR)/lib-log.Po -MT$@ \ + -o $@ $(LIB_DIR)/log/src/log.c + +lib-commandline.o: $(COMMANDLINE_SRC) + $(CC) $(CFLAGS) -c -MMD -MP -MF$(DEPDIR)/lib-commandline.Po -MT$@ \ + -o $@ $(LIB_DIR)/subcommands.c/commandline.c + +lib-parson.o: $(PARSON_SRC) + $(CC) $(CFLAGS) -c -MMD -MP -MF$(DEPDIR)/lib-parson.Po -MT$@ \ + -o $@ $(LIB_DIR)/parson/parson.c diff --git a/src/bin/common/debian.c b/src/bin/common/debian.c new file mode 100644 index 000000000..e22118b4c --- /dev/null +++ b/src/bin/common/debian.c @@ -0,0 +1,717 @@ +/* + * src/bin/pg_autoctl/debian.c + * + * Debian specific code to support registering a pg_autoctl node from a + * Postgres cluster created with pg_createcluster. We need to move the + * configuration files back to PGDATA. + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the PostgreSQL License. + * + */ +#include + +#include "postgres_fe.h" +#include "pqexpbuffer.h" + +#include "debian.h" +#include "keeper.h" +#include "keeper_config.h" +#include "parsing.h" + +#define EDITED_BY_PG_AUTOCTL "# edited by pg_auto_failover \n" + +static bool debian_find_postgres_configuration_files(PostgresSetup *pgSetup, + PostgresConfigFiles *pgConfigFiles); + +static bool debian_init_postgres_config_files(PostgresSetup *pgSetup, + PostgresConfigFiles *pgConfFiles, + PostgresConfigurationKind confKind); + +static bool buildDebianDataAndConfDirectoryNames(PostgresSetup *pgSetup, + DebianPathnames *debPathnames); + +static bool expandDebianPatterns(DebianPathnames *debPathnames, + const char *dataDirectoryTemplate, + const char *confDirectoryTemplate); + +static bool expandDebianPatternsInDirectoryName(char *pathname, + int pathnameSize, + const char *template, + const char *versionName, + const char *clusterName); + +static void initPostgresConfigFiles(const char *dirname, + PostgresConfigFiles *pgConfigFiles, + PostgresConfigurationKind kind); + +static bool postgresConfigFilesAllExist(PostgresConfigFiles *pgConfigFiles); + +static bool move_configuration_files(PostgresConfigFiles *src, + PostgresConfigFiles *dst); + +static bool comment_out_configuration_parameters(const char *srcConfPath, + const char *dstConfPath); + +static bool disableAutoStart(PostgresConfigFiles *pgConfigFiles); + + +/* + * keeper_ensure_pg_configuration_files_in_pgdata checks if postgresql.conf, + * pg_hba.conf, pg_ident.conf files exist in $PGDATA, if not it tries to get + * them from default location and modifies paths inside copied postgresql.conf. + */ +bool +keeper_ensure_pg_configuration_files_in_pgdata(PostgresSetup *pgSetup) +{ + PostgresConfigFiles pgConfigFiles = { 0 }; + + if (!debian_find_postgres_configuration_files(pgSetup, &pgConfigFiles)) + { + /* errors have already been logged */ + return false; + } + + switch (pgConfigFiles.kind) + { + case PG_CONFIG_TYPE_POSTGRES: + { + /* that's it, we're good */ + return true; + } + + case PG_CONFIG_TYPE_DEBIAN: + { + /* + * So now pgConfigFiles is the debian path for configuration files, + * and we're building a new pgdataConfigFiles for the Postgres + * configuration files in PGDATA. + */ + PostgresConfigFiles pgdataConfigFiles = { 0 }; + + log_info("Found a debian style installation in PGDATA \"%s\" with " + "postgresql.conf located at \"%s\"", + pgSetup->pgdata, + pgConfigFiles.conf); + + initPostgresConfigFiles(pgSetup->pgdata, + &pgdataConfigFiles, + PG_CONFIG_TYPE_POSTGRES); + + log_info("Moving configuration files back to PGDATA at \"%s\"", + pgSetup->pgdata); + + /* move configuration files back to PGDATA, or die trying */ + if (!move_configuration_files(&pgConfigFiles, + &pgdataConfigFiles)) + { + char *_dirname = dirname(pgConfigFiles.conf); + + log_fatal("Failed to move the debian configuration files from " + "\"%s\" back to PGDATA at \"%s\"", + _dirname, + pgSetup->pgdata); + return false; + } + + /* also disable debian auto start of the cluster we now own */ + if (!disableAutoStart(&pgConfigFiles)) + { + log_fatal("Failed to disable debian auto-start behavior, " + "see above for details"); + return false; + } + + return true; + } + + case PG_CONFIG_TYPE_UNKNOWN: + { + log_fatal("Failed to find the \"postgresql.conf\" file. " + "It's not in PGDATA, and it's not in the debian " + "place we had a look at. See above for details"); + return false; + } + } + + /* This is a huge bug */ + log_error("BUG: some unknown PG_CONFIG enum value was encountered"); + return false; +} + + +/* + * debian_find_postgres_configuration_files finds the Postgres configuration + * files following the following strategies: + * + * - first attempt to find the files where we expect them, in PGDATA + * - then attempt to find the files in the debian /etc/postgresql/%v/%c place + * + * At the moment we only have those two strategies, and with some luck that's + * all we're ever going to need. + */ +static bool +debian_find_postgres_configuration_files(PostgresSetup *pgSetup, + PostgresConfigFiles *pgConfigFiles) +{ + PostgresConfigFiles postgresConfFiles = { 0 }; + PostgresConfigFiles debianConfFiles = { 0 }; + + pgConfigFiles->kind = PG_CONFIG_TYPE_UNKNOWN; + + if (!pg_setup_pgdata_exists(pgSetup)) + { + return PG_CONFIG_TYPE_UNKNOWN; + } + + /* is it a Postgres core initdb style setup? */ + if (debian_init_postgres_config_files(pgSetup, + &postgresConfFiles, + PG_CONFIG_TYPE_POSTGRES)) + { + /* so we're dealing with a "normal" Postgres installation */ + *pgConfigFiles = postgresConfFiles; + + return true; + } + + /* + * Is it a debian postgresql-common style setup then? + * + * We only search for debian style setup when the main postgresql.conf file + * was not found. The previous call to debian_init_postgres_config_files + * might see a partial failure because of e.g. missing only pg_ident.conf. + */ + if (!file_exists(postgresConfFiles.conf)) + { + if (debian_init_postgres_config_files(pgSetup, + &debianConfFiles, + PG_CONFIG_TYPE_DEBIAN)) + { + /* so we're dealing with a "debian style" Postgres installation */ + *pgConfigFiles = debianConfFiles; + + return true; + } + } + + /* well that's all we know how to detect at this point */ + return false; +} + + +/* + * debian_init_postgres_config_files initializes the given PostgresConfigFiles + * structure with the location of existing files as found on-disk given a + * Postgres configuration kind. + */ +static bool +debian_init_postgres_config_files(PostgresSetup *pgSetup, + PostgresConfigFiles *pgConfigFiles, + PostgresConfigurationKind confKind) +{ + const char *pgdata = pgSetup->pgdata; + + switch (confKind) + { + case PG_CONFIG_TYPE_UNKNOWN: + { + /* that's a bug really */ + log_error("BUG: debian_init_postgres_config_files " + "called with UNKNOWN conf kind"); + return false; + } + + case PG_CONFIG_TYPE_POSTGRES: + { + initPostgresConfigFiles(pgdata, pgConfigFiles, + PG_CONFIG_TYPE_POSTGRES); + + return postgresConfigFilesAllExist(pgConfigFiles); + } + + case PG_CONFIG_TYPE_DEBIAN: + { + DebianPathnames debPathnames = { 0 }; + + if (!buildDebianDataAndConfDirectoryNames(pgSetup, &debPathnames)) + { + log_warn("Failed to match PGDATA at \"%s\" with a debian " + "setup following the data_directory template " + "'/var/lib/postgresql/%%v/%%c'", + pgSetup->pgdata); + return false; + } + + initPostgresConfigFiles(debPathnames.confDirectory, + pgConfigFiles, PG_CONFIG_TYPE_DEBIAN); + + return postgresConfigFilesAllExist(pgConfigFiles); + } + } + + /* This is a huge bug */ + log_error("BUG: some unknown PG_CONFIG enum value was encountered"); + return false; +} + + +/* + * buildDebianDataAndConfDirectoryNames builds the debian specific directory + * pathnames from the pgSetup pgdata location. + * + * For a debian cluster, we first have to extract the "cluster" name (%c) and + * then find the configuration files in /etc/postgresql/%v/%c with %v being the + * version number. + * + * Note that debian's /etc/postgresql-common/createcluster.conf defaults to + * using the following setup, and that's the only one we support at this + * moment. + * + * data_directory = '/var/lib/postgresql/%v/%c' + * + */ +static bool +buildDebianDataAndConfDirectoryNames(PostgresSetup *pgSetup, + DebianPathnames *debPathnames) +{ + char *pgmajor = strdup(pgSetup->pg_version); + + char pgdata[MAXPGPATH]; + + char clusterDir[MAXPGPATH] = { 0 }; + char versionDir[MAXPGPATH] = { 0 }; + + if (pgmajor == NULL) + { + log_error(ALLOCATION_FAILED_ERROR); + return false; + } + + /* we need to work with the absolute pathname of PGDATA */ + if (!normalize_filename(pgSetup->pgdata, pgdata, MAXPGPATH)) + { + /* errors have already been logged */ + return false; + } + + /* clusterDir is the same as pgdata really */ + strlcpy(clusterDir, pgdata, MAXPGPATH); + + /* from PGDATA, get the directory one-level up */ + strlcpy(versionDir, clusterDir, MAXPGPATH); + get_parent_directory(versionDir); + + /* get the names of our version and cluster directories */ + char *clusterDirName = strdup(basename(clusterDir)); + char *versionDirName = strdup(basename(versionDir)); + + if (clusterDirName == NULL || versionDirName == NULL) + { + log_error(ALLOCATION_FAILED_ERROR); + return false; + } + + /* transform pgversion "11.4" to "11" to get the major version part */ + char *dot = strchr(pgmajor, '.'); + + if (dot) + { + *dot = '\0'; + } + + /* check that debian pathname version string == Postgres version string */ + if (strcmp(versionDirName, pgmajor) != 0) + { + log_debug("Failed to match the version component of the " + "debian data_directory \"%s\" with the current " + "version of Postgres: \"%s\"", + pgdata, + pgmajor); + return false; + } + + /* prepare given debPathnames */ + strlcpy(debPathnames->versionName, versionDirName, PG_VERSION_STRING_MAX); + strlcpy(debPathnames->clusterName, clusterDirName, MAXPGPATH); + + if (!expandDebianPatterns(debPathnames, + "/var/lib/postgresql/%v/%c", + "/etc/postgresql/%v/%c")) + { + /* errors have already been logged */ + return false; + } + + /* free memory allocated with strdup */ + free(pgmajor); + free(clusterDirName); + free(versionDirName); + + return true; +} + + +/* + * expandDebianPatterns expands the %v and %c values in given templates and + * apply the result to debPathnames->dataDirectory and + * debPathnames->confDirectory. + */ +static bool +expandDebianPatterns(DebianPathnames *debPathnames, + const char *dataDirectoryTemplate, + const char *confDirectoryTemplate) +{ + return expandDebianPatternsInDirectoryName(debPathnames->dataDirectory, + MAXPGPATH, + dataDirectoryTemplate, + debPathnames->versionName, + debPathnames->clusterName) + + && expandDebianPatternsInDirectoryName(debPathnames->confDirectory, + MAXPGPATH, + confDirectoryTemplate, + debPathnames->versionName, + debPathnames->clusterName); +} + + +/* + * expandDebianPatternsInDirectoryName prepares a debian target data_directory + * or configuration directory from a pattern. + * + * Given the parameters: + * template = "/var/lib/postgresql/%v/%c" + * versionName = "11" + * clusterName = "main" + * + * Then the following string is copied in pre-allocated pathname: + * "/var/lib/postgresql/11/main" + */ +static bool +expandDebianPatternsInDirectoryName(char *pathname, + int pathnameSize, + const char *template, + const char *versionName, + const char *clusterName) +{ + int pathnameIndex = 0; + int templateIndex = 0; + int templateSize = strlen(template); + bool previousCharIsPercent = false; + + for (templateIndex = 0; templateIndex < templateSize; templateIndex++) + { + char currentChar = template[templateIndex]; + + if (pathnameIndex >= pathnameSize) + { + log_error("BUG: expandDebianPatternsInDirectoryName destination " + "buffer is too short (%d bytes)", pathnameSize); + return false; + } + + if (previousCharIsPercent) + { + switch (currentChar) + { + case 'v': + { + int versionSize = strlen(versionName); + + /* + * Only copy if we have enough room, increment pathnameSize + * anyways so that the first check in the main loop catches + * and report the error. + */ + if ((pathnameIndex + versionSize) < pathnameSize) + { + strlcpy(pathname + pathnameIndex, versionName, pathnameSize - + pathnameIndex); + pathnameIndex += versionSize; + } + break; + } + + case 'c': + { + int clusterSize = strlen(clusterName); + + /* + * Only copy if we have enough room, increment pathnameSize + * anyways so that the first check in the main loop catches + * and report the error. + */ + if ((pathnameIndex + clusterSize) < pathnameSize) + { + strlcpy(pathname + pathnameIndex, clusterName, pathnameSize - + pathnameIndex); + pathnameIndex += clusterSize; + } + break; + } + + default: + { + pathname[pathnameIndex++] = currentChar; + break; + } + } + } + else if (currentChar != '%') + { + pathname[pathnameIndex++] = currentChar; + } + + previousCharIsPercent = currentChar == '%'; + } + + return true; +} + + +/* + * initPostgresConfigFiles initializes PostgresConfigFiles structure with our + * filenames located in given directory pathname. + */ +static void +initPostgresConfigFiles(const char *dirname, + PostgresConfigFiles *pgConfigFiles, + PostgresConfigurationKind confKind) +{ + pgConfigFiles->kind = confKind; + join_path_components(pgConfigFiles->conf, dirname, "postgresql.conf"); + join_path_components(pgConfigFiles->ident, dirname, "pg_ident.conf"); + join_path_components(pgConfigFiles->hba, dirname, "pg_hba.conf"); +} + + +/* + * postgresConfigFilesAllExist returns true when the three files that we track + * all exit on the file system, per file_exists() test. + */ +static bool +postgresConfigFilesAllExist(PostgresConfigFiles *pgConfigFiles) +{ + /* + * WARN the user about the unexpected nature of our setup here, even if we + * then move on to make it the way we expect it. + */ + if (!file_exists(pgConfigFiles->conf)) + { + log_warn("Failed to find Postgres configuration files in PGDATA, " + "as expected: \"%s\" does not exist", + pgConfigFiles->conf); + } + + if (!file_exists(pgConfigFiles->ident)) + { + log_warn("Failed to find Postgres configuration files in PGDATA, " + "as expected: \"%s\" does not exist", + pgConfigFiles->ident); + } + + if (!file_exists(pgConfigFiles->hba)) + { + log_warn("Failed to find Postgres configuration files in PGDATA, " + "as expected: \"%s\" does not exist", + pgConfigFiles->hba); + } + + return file_exists(pgConfigFiles->conf) && + file_exists(pgConfigFiles->ident) && + file_exists(pgConfigFiles->hba); +} + + +/* + * move_configuration_files moves configuration files from the source place to + * the destination place as given. + * + * While moving the files, we also need to edit the "postgresql.conf" content + * to comment out the lines for the config_file, hba_file, and ident_file + * location. We're going to use the Postgres defaults in PGDATA. + */ +static bool +move_configuration_files(PostgresConfigFiles *src, PostgresConfigFiles *dst) +{ + /* edit postgresql.conf and move it to its dst pathname */ + log_info("Preparing \"%s\" from \"%s\"", dst->conf, src->conf); + + if (!comment_out_configuration_parameters(src->conf, dst->conf)) + { + return false; + } + + /* HBA and ident files are copied without edits */ + log_info("Moving \"%s\" to \"%s\"", src->hba, dst->hba); + + if (!move_file(src->hba, dst->hba)) + { + /* + * Clean-up the mess then, and return false whether the clean-up is a + * success or not. + */ + (void) unlink_file(dst->conf); + + return false; + } + + + /* HBA and ident files are copied without edits */ + log_info("Moving \"%s\" to \"%s\"", src->ident, dst->ident); + + if (!move_file(src->ident, dst->ident)) + { + /* + * Clean-up the mess then, and return false whether the clean-up is a + * success or not. + */ + (void) unlink_file(dst->conf); + (void) move_file(dst->hba, src->hba); + + return false; + } + + /* finish the move of the postgresql.conf */ + if (!unlink_file(src->conf)) + { + /* + * Clean-up the mess then, and return false whether the clean-up is a + * success or not. + */ + (void) move_file(dst->hba, src->hba); + (void) move_file(dst->ident, src->ident); + return false; + } + + /* consider failure to symlink as a non-fatal event */ + (void) create_symbolic_link(src->conf, dst->conf); + (void) create_symbolic_link(src->ident, dst->ident); + (void) create_symbolic_link(src->hba, dst->hba); + + return true; +} + + +/* + * comment_out_configuration_parameters reads postgresql.conf file from source + * location and writes a new version of it at destination location with some + * parameters commented out: + * + * data_directory + * config_file + * hba_file + * ident_file + * include_dir + */ +static bool +comment_out_configuration_parameters(const char *srcConfPath, + const char *dstConfPath) +{ + char lineBuffer[BUFSIZE]; + + /* + * configuration parameters can appear in any order, and we + * need to check for patterns for NAME = VALUE and NAME=VALUE + */ + char *targetVariableExpression = + "(" + "data_directory" + "|hba_file" + "|ident_file" + "|include_dir" + "|stats_temp_directory" + ")( *)="; + + /* open a file */ + FILE *fileStream = fopen_read_only(srcConfPath); + if (fileStream == NULL) + { + log_error("Failed to open file \"%s\": %m", srcConfPath); + return false; + } + + PQExpBuffer newConfContents = createPQExpBuffer(); + if (newConfContents == NULL) + { + log_error("Failed to allocate memory"); + return false; + } + + /* read each line including terminating new line and process it */ + while (fgets(lineBuffer, BUFSIZE, fileStream) != NULL) + { + bool variableFound = false; + char *matchedString = + regexp_first_match(lineBuffer, targetVariableExpression); + + /* check if the line contains any of target variables */ + if (matchedString != NULL) + { + variableFound = true; + + /* regexp_first_match uses malloc, result must be deallocated */ + free(matchedString); + } + + /* + * comment out the line if any of target variables is found + * and if it was not already commented + */ + if (variableFound && lineBuffer[0] != '#') + { + appendPQExpBufferStr(newConfContents, EDITED_BY_PG_AUTOCTL); + appendPQExpBufferStr(newConfContents, "# "); + } + + /* copy rest of the line */ + appendPQExpBufferStr(newConfContents, lineBuffer); + } + + fclose(fileStream); + + /* write the resulting content at the destination path */ + if (!write_file(newConfContents->data, newConfContents->len, dstConfPath)) + { + destroyPQExpBuffer(newConfContents); + return false; + } + + /* we don't need the buffer anymore */ + destroyPQExpBuffer(newConfContents); + + /* + * Refrain from removing the source file, we might fail to proceed and then + * we will want to offer a path forward to the user where the original + * configuration file is still around + */ + + return true; +} + + +/* + * disableAutoStart disables auto start in default configuration + */ +static bool +disableAutoStart(PostgresConfigFiles *pgConfigFiles) +{ + char startConfPath[MAXPGPATH] = { 0 }; + char copyStartConfPath[MAXPGPATH] = { 0 }; + char *newStartConfData = EDITED_BY_PG_AUTOCTL "disabled"; + + path_in_same_directory(pgConfigFiles->conf, "start.conf", startConfPath); + path_in_same_directory(pgConfigFiles->conf, + "start.conf.orig", copyStartConfPath); + + if (rename(startConfPath, copyStartConfPath) != 0) + { + log_error("Failed to rename debian auto start setup to \"%s\": %m", + copyStartConfPath); + + return false; + } + + return write_file(newStartConfData, strlen(newStartConfData), startConfPath); +} diff --git a/src/bin/common/debian.h b/src/bin/common/debian.h new file mode 100644 index 000000000..9db36c785 --- /dev/null +++ b/src/bin/common/debian.h @@ -0,0 +1,68 @@ +/* + * src/bin/pg_autoctl/debian.h + * + * Debian specific code to support registering a pg_autoctl node from a + * Postgres cluster created with pg_createcluster. We need to move the + * configuration files back to PGDATA. + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the PostgreSQL License. + * + */ + +#ifndef DEBIAN_H +#define DEBIAN_H + +#include "keeper_config.h" +#include "pgsetup.h" + +/* + * We know how to find configuration files in either PGDATA as per Postgres + * core, or in the debian cluster configuration directory as per debian + * postgres-common packaging, implemented in pg_createcluster. + */ +typedef enum +{ + PG_CONFIG_TYPE_UNKNOWN = 0, + PG_CONFIG_TYPE_POSTGRES, + PG_CONFIG_TYPE_DEBIAN +} PostgresConfigurationKind; + +/* + * debian's pg_createcluster moves the 3 configuration files to a place in /etc: + * + * - postgresql.conf + * - pg_ident.conf + * - pg_hba.conf + * + * On top of that debian also manages a "start.conf" file to decide if their + * systemd integration should manage a given cluster. + */ +typedef struct pg_config_files +{ + PostgresConfigurationKind kind; + char conf[MAXPGPATH]; + char ident[MAXPGPATH]; + char hba[MAXPGPATH]; +} PostgresConfigFiles; + + +/* + * debian handles paths for data_directory and configuration directory that + * depend on two components: Postgres version string ("11", "12", etc) and + * debian cluster name (defaults to "main"). + */ +typedef struct debian_pathnames +{ + char versionName[PG_VERSION_STRING_MAX]; + char clusterName[MAXPGPATH]; + + char dataDirectory[MAXPGPATH]; + char confDirectory[MAXPGPATH]; +} DebianPathnames; + + +bool keeper_ensure_pg_configuration_files_in_pgdata(PostgresSetup *pgSetup); + + +#endif /* DEBIAN_H */ diff --git a/src/bin/common/env_utils.c b/src/bin/common/env_utils.c new file mode 100644 index 000000000..2b1f22109 --- /dev/null +++ b/src/bin/common/env_utils.c @@ -0,0 +1,166 @@ +/* + * src/bin/pg_autoctl/env_utils.c + * Utility functions for interacting with environment settings. + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the PostgreSQL License. + * + */ + +#include +#include + +#include "defaults.h" +#include "env_utils.h" +#include "log.h" + + +/* + * env_found_empty returns true if the passed environment variable is the empty + * string. It returns false when the environment variable is not set or if it + * set but is something else than the empty string. + */ +bool +env_found_empty(const char *name) +{ + if (name == NULL || strlen(name) == 0) + { + log_error("Failed to get environment setting. " + "NULL or empty variable name is provided"); + return false; + } + + /* + * Explanation of IGNORE-BANNED + * getenv is safe here because we never provide null argument, + * and only check the value it's length. + */ + char *envvalue = getenv(name); /* IGNORE-BANNED */ + return envvalue != NULL && strlen(envvalue) == 0; +} + + +/* + * env_exists returns true if the passed environment variable exists in the + * environment, otherwise it returns false. + */ +bool +env_exists(const char *name) +{ + if (name == NULL || strlen(name) == 0) + { + log_error("Failed to get environment setting. " + "NULL or empty variable name is provided"); + return false; + } + + /* + * Explanation of IGNORE-BANNED + * getenv is safe here because we never provide null argument, + * and only check if it returns NULL. + */ + return getenv(name) != NULL; /* IGNORE-BANNED */ +} + + +/* + * get_env_copy_with_fallback copies the environment variable with "name" into + * the result buffer. It returns false when it fails. If the environment + * variable is not set the fallback string will be written in the buffer. + * Except when fallback is NULL, in that case an error is returned. + */ +bool +get_env_copy_with_fallback(const char *name, char *result, int maxLength, + const char *fallback) +{ + if (name == NULL || strlen(name) == 0) + { + log_error("Failed to get environment setting. " + "NULL or empty variable name is provided"); + return false; + } + + if (result == NULL) + { + log_error("Failed to get environment setting. " + "Tried to store in NULL pointer"); + return false; + } + + /* + * Explanation of IGNORE-BANNED + * getenv is safe here because we never provide null argument, + * and copy out the result immediately. + */ + const char *envvalue = getenv(name); /* IGNORE-BANNED */ + if (envvalue == NULL) + { + envvalue = fallback; + if (envvalue == NULL) + { + log_error("Failed to get value for environment variable '%s', " + "which is unset", name); + return false; + } + } + + size_t actualLength = strlcpy(result, envvalue, maxLength); + + /* uses >= to make sure the nullbyte fits */ + if (actualLength >= maxLength) + { + log_error("Failed to copy value stored in %s environment setting, " + "which is %lu long. pg_autoctl only supports %lu bytes for " + "this environment setting", + name, + (unsigned long) actualLength, + (unsigned long) maxLength - 1); + return false; + } + return true; +} + + +/* + * get_env_copy copies the environmennt variable with "name" into tho result + * buffer. It returns false when it fails. The environment variable not + * existing is also considered a failure. + */ +bool +get_env_copy(const char *name, char *result, int maxLength) +{ + return get_env_copy_with_fallback(name, result, maxLength, NULL); +} + + +/* + * get_env_pgdata checks for environment value PGDATA + * and copy its value into provided buffer. + * + * function returns true on successful run. returns false + * if it can't find PGDATA or its value is larger than + * the provided buffer + */ +bool +get_env_pgdata(char *pgdata) +{ + return get_env_copy("PGDATA", pgdata, MAXPGPATH) > 0; +} + + +/* + * get_env_pgdata_or_exit does the same as get_env_pgdata. Instead of + * returning false in case of error it exits the process and shows a FATAL log + * message. + */ +void +get_env_pgdata_or_exit(char *pgdata) +{ + if (get_env_pgdata(pgdata)) + { + return; + } + log_fatal("Failed to set PGDATA either from the environment " + "or from --pgdata"); + exit(EXIT_CODE_BAD_ARGS); +} diff --git a/src/bin/common/env_utils.h b/src/bin/common/env_utils.h new file mode 100644 index 000000000..6dee908ac --- /dev/null +++ b/src/bin/common/env_utils.h @@ -0,0 +1,22 @@ +/* + * src/bin/pg_autoctl/env_utils.h + * Utility functions for interacting with environment settings. + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the PostgreSQL License. + * + */ + +#ifndef ENV_UTILS_H +#define ENV_UTILS_H + +#include "postgres_fe.h" + +bool env_found_empty(const char *name); +bool env_exists(const char *name); +bool get_env_copy(const char *name, char *outbuffer, int maxLength); +bool get_env_copy_with_fallback(const char *name, char *result, int maxLength, + const char *fallback); +bool get_env_pgdata(char *pgdata); +void get_env_pgdata_or_exit(char *pgdata); +#endif /* ENV_UTILS_H */ diff --git a/src/bin/common/file_utils.c b/src/bin/common/file_utils.c new file mode 100644 index 000000000..70527168f --- /dev/null +++ b/src/bin/common/file_utils.c @@ -0,0 +1,943 @@ +/* + * src/bin/pg_autoctl/file_utils.c + * Implementations of utility functions for reading and writing files + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the PostgreSQL License. + * + */ + +#include +#include +#include + +#if defined(__APPLE__) +#include +#endif + +#include "postgres_fe.h" + +#include "snprintf.h" + +#include "cli_root.h" +#include "defaults.h" +#include "env_utils.h" +#include "file_utils.h" +#include "log.h" + +static bool read_file_internal(FILE *fileStream, + const char *filePath, + char **contents, + long *fileSize); + +/* + * file_exists returns true if the given filename is known to exist + * on the file system or false if it does not exist or in case of + * error. + */ +bool +file_exists(const char *filename) +{ + bool exists = access(filename, F_OK) != -1; + if (!exists && errno != 0) + { + /* + * Only log "interesting" errors here. + * + * The fact that the file does not exist is not interesting: we're + * retuning false and the caller figures it out, maybe then creating + * the file. + */ + if (errno != ENOENT && errno != ENOTDIR) + { + log_error("Failed to check if file \"%s\" exists: %m", filename); + } + return false; + } + + return exists; +} + + +/* + * directory_exists returns whether the given path is the name of a directory that + * exists on the file system or not. + */ +bool +directory_exists(const char *path) +{ + struct stat info; + + if (!file_exists(path)) + { + return false; + } + + if (stat(path, &info) != 0) + { + log_error("Failed to stat \"%s\": %m\n", path); + return false; + } + + bool result = (info.st_mode & S_IFMT) == S_IFDIR; + return result; +} + + +/* + * ensure_empty_dir ensures that the given path points to an empty directory with + * the given mode. If it fails to do so, it returns false. + */ +bool +ensure_empty_dir(const char *dirname, int mode) +{ + /* pg_mkdir_p might modify its input, so create a copy of dirname. */ + char dirname_copy[MAXPGPATH]; + strlcpy(dirname_copy, dirname, MAXPGPATH); + + if (directory_exists(dirname)) + { + if (!rmtree(dirname, true)) + { + log_error("Failed to remove directory \"%s\": %m", dirname); + return false; + } + } + else + { + /* + * reset errno, we don't care anymore that it failed because dirname + * doesn't exists. + */ + errno = 0; + } + + if (pg_mkdir_p(dirname_copy, mode) == -1) + { + log_error("Failed to ensure empty directory \"%s\": %m", dirname); + return false; + } + + return true; +} + + +/* + * fopen_with_umask is a version of fopen that gives more control. The main + * advantage of it is that it allows specifying a umask of the file. This makes + * sure files are not accidentally created with umask 777 if the user has it + * configured in a weird way. + * + * This function returns NULL when opening the file fails. So this should be + * handled. It will log an error in this case though, so that's not necessary + * at the callsite. + */ +FILE * +fopen_with_umask(const char *filePath, const char *modes, int flags, mode_t umask) +{ + int fileDescriptor = open(filePath, flags, umask); + if (fileDescriptor == -1) + { + log_error("Failed to open file \"%s\": %m", filePath); + return NULL; + } + + FILE *fileStream = fdopen(fileDescriptor, modes); + if (fileStream == NULL) + { + log_error("Failed to open file \"%s\": %m", filePath); + close(fileDescriptor); + } + return fileStream; +} + + +/* + * fopen_read_only opens the file as a read only stream. + */ +FILE * +fopen_read_only(const char *filePath) +{ + /* + * Explanation of IGNORE-BANNED + * fopen is safe here because we open the file in read only mode. So no + * exclusive access is needed. + */ + return fopen(filePath, "rb"); /* IGNORE-BANNED */ +} + + +/* + * write_file writes the given data to the file given by filePath using + * our logging library to report errors. If succesful, the function returns + * true. + */ +bool +write_file(char *data, long fileSize, const char *filePath) +{ + FILE *fileStream = fopen_with_umask(filePath, "wb", FOPEN_FLAGS_W, 0644); + + if (fileStream == NULL) + { + /* errors have already been logged */ + return false; + } + + if (fwrite(data, sizeof(char), fileSize, fileStream) < fileSize) + { + log_error("Failed to write file \"%s\": %m", filePath); + fclose(fileStream); + return false; + } + + if (fclose(fileStream) == EOF) + { + log_error("Failed to write file \"%s\"", filePath); + return false; + } + + return true; +} + + +/* + * append_to_file writes the given data to the end of the file given by + * filePath using our logging library to report errors. If succesful, the + * function returns true. + */ +bool +append_to_file(char *data, long fileSize, const char *filePath) +{ + FILE *fileStream = fopen_with_umask(filePath, "ab", FOPEN_FLAGS_A, 0644); + + if (fileStream == NULL) + { + /* errors have already been logged */ + return false; + } + + if (fwrite(data, sizeof(char), fileSize, fileStream) < fileSize) + { + log_error("Failed to write file \"%s\": %m", filePath); + fclose(fileStream); + return false; + } + + if (fclose(fileStream) == EOF) + { + log_error("Failed to write file \"%s\"", filePath); + return false; + } + + return true; +} + + +/* + * read_file_if_exists is a utility function that reads the contents of a file + * using our logging library to report errors. ENOENT is not considered worth + * of a log message in this function, and we still return false in that case. + * + * If successful, the function returns true and fileSize points to the number + * of bytes that were read and contents points to a buffer containing the entire + * contents of the file. This buffer should be freed by the caller. + */ +bool +read_file_if_exists(const char *filePath, char **contents, long *fileSize) +{ + /* open a file */ + FILE *fileStream = fopen_read_only(filePath); + + if (fileStream == NULL) + { + if (errno != ENOENT) + { + log_error("Failed to open file \"%s\": %m", filePath); + } + return false; + } + + return read_file_internal(fileStream, filePath, contents, fileSize); +} + + +/* + * read_file is a utility function that reads the contents of a file using our + * logging library to report errors. + * + * If successful, the function returns true and fileSize points to the number + * of bytes that were read and contents points to a buffer containing the entire + * contents of the file. This buffer should be freed by the caller. + */ +bool +read_file(const char *filePath, char **contents, long *fileSize) +{ + /* open a file */ + FILE *fileStream = fopen_read_only(filePath); + if (fileStream == NULL) + { + log_error("Failed to open file \"%s\": %m", filePath); + return false; + } + + return read_file_internal(fileStream, filePath, contents, fileSize); +} + + +/* + * read_file_internal is shared by both read_file and read_file_if_exists + * functions. + */ +static bool +read_file_internal(FILE *fileStream, + const char *filePath, char **contents, long *fileSize) +{ + /* get the file size */ + if (fseek(fileStream, 0, SEEK_END) != 0) + { + log_error("Failed to read file \"%s\": %m", filePath); + fclose(fileStream); + return false; + } + + *fileSize = ftell(fileStream); + if (*fileSize < 0) + { + log_error("Failed to read file \"%s\": %m", filePath); + fclose(fileStream); + return false; + } + + if (fseek(fileStream, 0, SEEK_SET) != 0) + { + log_error("Failed to read file \"%s\": %m", filePath); + fclose(fileStream); + return false; + } + + /* read the contents */ + char *data = malloc(*fileSize + 1); + if (data == NULL) + { + log_error("Failed to allocate %ld bytes", *fileSize); + log_error(ALLOCATION_FAILED_ERROR); + fclose(fileStream); + return false; + } + + if (fread(data, sizeof(char), *fileSize, fileStream) < *fileSize) + { + log_error("Failed to read file \"%s\": %m", filePath); + fclose(fileStream); + free(data); + return false; + } + + if (fclose(fileStream) == EOF) + { + log_error("Failed to read file \"%s\"", filePath); + free(data); + return false; + } + + data[*fileSize] = '\0'; + *contents = data; + + return true; +} + + +/* + * move_file is a utility function to move a file from sourcePath to + * destinationPath. It behaves like mv system command. First attempts to move + * a file using rename. if it fails with EXDEV error, the function duplicates + * the source file with owner and permission information and removes it. + */ +bool +move_file(char *sourcePath, char *destinationPath) +{ + if (strncmp(sourcePath, destinationPath, MAXPGPATH) == 0) + { + /* nothing to do */ + log_warn("Source and destination are the same \"%s\", nothing to move.", + sourcePath); + return true; + } + + if (!file_exists(sourcePath)) + { + log_error("Failed to move file, source file \"%s\" does not exist.", + sourcePath); + return false; + } + + if (file_exists(destinationPath)) + { + log_error("Failed to move file, destination file \"%s\" already exists.", + destinationPath); + return false; + } + + /* first try atomic move operation */ + if (rename(sourcePath, destinationPath) == 0) + { + return true; + } + + /* + * rename fails with errno = EXDEV when moving file to a different file + * system. + */ + if (errno != EXDEV) + { + log_error("Failed to move file \"%s\" to \"%s\": %m", + sourcePath, destinationPath); + return false; + } + + if (!duplicate_file(sourcePath, destinationPath)) + { + /* specific error is already logged */ + log_error("Canceling file move due to errors."); + return false; + } + + /* everything is successful we can remove the file */ + unlink_file(sourcePath); + + return true; +} + + +/* + * duplicate_file is a utility function to duplicate a file from sourcePath to + * destinationPath. It reads the contents of the source file and writes to the + * destination file. It expects non-existing destination file and does not + * copy over if it exists. The function returns true on successful execution. + * + * Note: the function reads the whole file into memory before copying out. + */ +bool +duplicate_file(char *sourcePath, char *destinationPath) +{ + char *fileContents; + long fileSize; + struct stat sourceFileStat; + + if (!read_file(sourcePath, &fileContents, &fileSize)) + { + /* errors are logged */ + return false; + } + + if (file_exists(destinationPath)) + { + log_error("Failed to duplicate, destination file already exists : %s", + destinationPath); + return false; + } + + bool foundError = !write_file(fileContents, fileSize, destinationPath); + + free(fileContents); + + if (foundError) + { + /* errors are logged in write_file */ + return false; + } + + /* set uid gid and mode */ + if (stat(sourcePath, &sourceFileStat) != 0) + { + log_error("Failed to get ownership and file permissions on \"%s\"", + sourcePath); + foundError = true; + } + else + { + if (chown(destinationPath, sourceFileStat.st_uid, sourceFileStat.st_gid) != 0) + { + log_error("Failed to set user and group id on \"%s\"", + destinationPath); + foundError = true; + } + if (chmod(destinationPath, sourceFileStat.st_mode) != 0) + { + log_error("Failed to set file permissions on \"%s\"", + destinationPath); + foundError = true; + } + } + + if (foundError) + { + /* errors are already logged */ + unlink_file(destinationPath); + return false; + } + + return true; +} + + +/* + * create_symbolic_link creates a symbolic link to source path. + */ +bool +create_symbolic_link(char *sourcePath, char *targetPath) +{ + if (symlink(sourcePath, targetPath) != 0) + { + log_error("Failed to create symbolic link to \"%s\": %m", targetPath); + return false; + } + return true; +} + + +/* + * path_in_same_directory constructs the path for a file with name fileName + * that is in the same directory as basePath, which should be an absolute + * path. The result is written to destinationPath, which should be at least + * MAXPATH in size. + */ +void +path_in_same_directory(const char *basePath, const char *fileName, + char *destinationPath) +{ + strlcpy(destinationPath, basePath, MAXPGPATH); + get_parent_directory(destinationPath); + join_path_components(destinationPath, destinationPath, fileName); +} + + +/* From PostgreSQL sources at src/port/path.c */ +#ifndef WIN32 +#define IS_PATH_VAR_SEP(ch) ((ch) == ':') +#else +#define IS_PATH_VAR_SEP(ch) ((ch) == ';') +#endif + + +/* + * search_path_first copies the first entry found in PATH to result. result + * should be a buffer of (at least) MAXPGPATH size. + * The function returns false and logs an error when it cannot find the command + * in PATH. + */ +bool +search_path_first(const char *filename, char *result, int logLevel) +{ + SearchPath paths = { 0 }; + + if (!search_path(filename, &paths) || paths.found == 0) + { + log_level(logLevel, "Failed to find %s command in your PATH", filename); + return false; + } + + strlcpy(result, paths.matches[0], MAXPGPATH); + + return true; +} + + +/* + * Searches all the directories in the PATH environment variable for the given + * filename. Returns number of occurrences and each match found with its + * fullname, including the given filename, in the given pre-allocated + * SearchPath result. + */ +bool +search_path(const char *filename, SearchPath *result) +{ + char pathlist[MAXPATHSIZE] = { 0 }; + + /* we didn't count nor find anything yet */ + result->found = 0; + + /* Create a copy of pathlist, because we modify it here. */ + if (!get_env_copy("PATH", pathlist, sizeof(pathlist))) + { + /* errors have already been logged */ + return false; + } + + char *path = pathlist; + + while (path != NULL) + { + char candidate[MAXPGPATH] = { 0 }; + char *sep = first_path_var_separator(path); + + /* split path on current token, null-terminating string at separator */ + if (sep != NULL) + { + *sep = '\0'; + } + + (void) join_path_components(candidate, path, filename); + (void) canonicalize_path(candidate); + + if (file_exists(candidate)) + { + strlcpy(result->matches[result->found++], candidate, MAXPGPATH); + } + + path = (sep == NULL ? NULL : sep + 1); + } + + return true; +} + + +/* + * search_path_deduplicate_symlinks traverse the SearchPath result obtained by + * calling the search_path() function and removes entries that are pointing to + * the same binary on-disk. + * + * In modern debian installations, for instance, we have /bin -> /usr/bin; and + * then we might find pg_config both in /bin/pg_config and /usr/bin/pg_config + * although it's only been installed once, and both are the same file. + * + * We use realpath() to deduplicate entries, and keep the entry that is not a + * symbolic link. + */ +bool +search_path_deduplicate_symlinks(SearchPath *results, SearchPath *dedup) +{ + /* now re-initialize the target structure dedup */ + dedup->found = 0; + + for (int rIndex = 0; rIndex < results->found; rIndex++) + { + bool alreadyThere = false; + + char *currentPath = results->matches[rIndex]; + char currentRealPath[PATH_MAX] = { 0 }; + + if (realpath(currentPath, currentRealPath) == NULL) + { + log_error("Failed to normalize file name \"%s\": %m", currentPath); + return false; + } + + /* add-in the realpath to dedup, unless it's already in there */ + for (int dIndex = 0; dIndex < dedup->found; dIndex++) + { + if (strcmp(dedup->matches[dIndex], currentRealPath) == 0) + { + alreadyThere = true; + + log_debug("dedup: skipping \"%s\"", currentPath); + break; + } + } + + if (!alreadyThere) + { + int bytesWritten = + strlcpy(dedup->matches[dedup->found++], + currentRealPath, + MAXPGPATH); + + if (bytesWritten >= MAXPGPATH) + { + log_error( + "Real path \"%s\" is %d bytes long, and pg_autoctl " + "is limited to handling paths of %d bytes long, maximum", + currentRealPath, + (int) strlen(currentRealPath), + MAXPGPATH); + + return false; + } + } + } + + return true; +} + + +/* + * unlink_state_file calls unlink(2) on the state file to make sure we don't + * leave a lingering state on-disk. + */ +bool +unlink_file(const char *filename) +{ + if (unlink(filename) == -1) + { + /* if it didn't exist yet, good news! */ + if (errno != ENOENT && errno != ENOTDIR) + { + log_error("Failed to remove file \"%s\": %m", filename); + return false; + } + } + + return true; +} + + +/* + * get_program_absolute_path returns the absolute path of the current program + * being executed. Note: the shell is responsible to set that in interactive + * environments, and when the pg_autoctl binary is in the PATH of the user, + * then argv[0] (here pg_autoctl_argv0) is just "pg_autoctl". + */ +bool +set_program_absolute_path(char *program, int size) +{ +#if defined(__APPLE__) + int actualSize = _NSGetExecutablePath(program, (uint32_t *) &size); + + if (actualSize != 0) + { + log_error("Failed to get absolute path for the pg_autoctl program, " + "absolute path requires %d bytes and we support paths up " + "to %d bytes only", actualSize, size); + return false; + } + + log_debug("Found absolute program: \"%s\"", program); + +#else + + /* + * On Linux and FreeBSD and Solaris, we can find a symbolic link to our + * program and get the information with readlink. Of course the /proc entry + * to read is not the same on both systems, so we try several things here. + */ + bool found = false; + char *procEntryCandidates[] = { + "/proc/self/exe", /* Linux */ + "/proc/curproc/file", /* FreeBSD */ + "/proc/self/path/a.out" /* Solaris */ + }; + int procEntrySize = sizeof(procEntryCandidates) / sizeof(char *); + int procEntryIndex = 0; + + for (procEntryIndex = 0; procEntryIndex < procEntrySize; procEntryIndex++) + { + if (readlink(procEntryCandidates[procEntryIndex], program, size) != -1) + { + found = true; + log_debug("Found absolute program \"%s\" in \"%s\"", + program, + procEntryCandidates[procEntryIndex]); + } + else + { + /* when the file does not exist, we try our next guess */ + if (errno != ENOENT && errno != ENOTDIR) + { + log_error("Failed to get absolute path for the " + "pg_autoctl program: %m"); + return false; + } + } + } + + if (found) + { + return true; + } + else + { + /* + * Now either return pg_autoctl_argv0 when that's an absolute filename, + * or search for it in the PATH otherwise. + */ + SearchPath paths = { 0 }; + + if (pg_autoctl_argv0[0] == '/') + { + strlcpy(program, pg_autoctl_argv0, size); + return true; + } + + if (!search_path(pg_autoctl_argv0, &paths) || paths.found == 0) + { + log_error("Failed to find \"%s\" in PATH environment", + pg_autoctl_argv0); + exit(EXIT_CODE_INTERNAL_ERROR); + } + else + { + log_debug("Found \"%s\" in PATH at \"%s\"", + pg_autoctl_argv0, paths.matches[0]); + strlcpy(program, paths.matches[0], size); + + return true; + } + } +#endif + + return true; +} + + +/* + * normalize_filename returns the real path of a given filename that belongs to + * an existing file on-disk, resolving symlinks and pruning double-slashes and + * other weird constructs. filename and dst are allowed to point to the same + * adress. + */ +bool +normalize_filename(const char *filename, char *dst, int size) +{ + /* normalize the path to the configuration file, if it exists */ + if (file_exists(filename)) + { + char realPath[PATH_MAX] = { 0 }; + + if (realpath(filename, realPath) == NULL) + { + log_fatal("Failed to normalize file name \"%s\": %m", filename); + return false; + } + + if (strlcpy(dst, realPath, size) >= size) + { + log_fatal("Real path \"%s\" is %d bytes long, and pg_autoctl " + "is limited to handling paths of %d bytes long, maximum", + realPath, (int) strlen(realPath), size); + return false; + } + } + else + { + char realPath[PATH_MAX] = { 0 }; + + /* protect against undefined behavior if dst overlaps with filename */ + strlcpy(realPath, filename, MAXPGPATH); + strlcpy(dst, realPath, MAXPGPATH); + } + + return true; +} + + +/* + * fformat is a secured down version of pg_fprintf: + * + * Additional security checks are: + * - make sure stream is not null + * - make sure fmt is not null + * - rely on pg_fprintf Assert() that %s arguments are not null + */ +int +fformat(FILE *stream, const char *fmt, ...) +{ + va_list args; + + if (stream == NULL || fmt == NULL) + { + log_error("BUG: fformat is called with a NULL target or format string"); + return -1; + } + + va_start(args, fmt); + int len = pg_vfprintf(stream, fmt, args); + va_end(args); + return len; +} + + +/* + * sformat is a secured down version of pg_snprintf + */ +int +sformat(char *str, size_t count, const char *fmt, ...) +{ + va_list args; + + if (str == NULL || fmt == NULL) + { + log_error("BUG: sformat is called with a NULL target or format string"); + return -1; + } + + va_start(args, fmt); + int len = pg_vsnprintf(str, count, fmt, args); + va_end(args); + + if (len >= count) + { + log_error("BUG: sformat needs %d bytes to expend format string \"%s\", " + "and a target string of %lu bytes only has been given.", + len, fmt, + (unsigned long) count); + } + + return len; +} + + +/* + * set_ps_title sets the process title seen in ps/top and friends, truncating + * if there is not enough space, rather than causing memory corruption. + * + * Inspired / stolen from Postgres code src/backend/utils/misc/ps_status.c with + * most of the portability bits removed. At the moment we prefer simple code + * that works on few targets to highly portable code. + */ +void +init_ps_buffer(int argc, char **argv) +{ +#if defined(__linux__) || defined(__darwin__) + char *end_of_area = NULL; + int i; + + /* + * check for contiguous argv strings + */ + for (i = 0; i < argc; i++) + { + if (i == 0 || end_of_area + 1 == argv[i]) + { + end_of_area = argv[i] + strlen(argv[i]); /* lgtm[cpp/tainted-arithmetic] */ + } + } + + if (end_of_area == NULL) /* probably can't happen? */ + { + ps_buffer = NULL; + ps_buffer_size = 0; + return; + } + + ps_buffer = argv[0]; + last_status_len = ps_buffer_size = end_of_area - argv[0]; /* lgtm[cpp/tainted-arithmetic] */ + +#else + ps_buffer = NULL; + ps_buffer_size = 0; + + return; +#endif +} + + +/* + * set_ps_title sets our process name visible in ps/top/pstree etc. + */ +void +set_ps_title(const char *title) +{ + if (ps_buffer == NULL) + { + /* noop */ + return; + } + + int n = sformat(ps_buffer, ps_buffer_size, "%s", title); + + /* pad our process title string */ + for (size_t i = n; i < ps_buffer_size; i++) + { + *(ps_buffer + i) = '\0'; + } +} diff --git a/src/bin/common/file_utils.h b/src/bin/common/file_utils.h new file mode 100644 index 000000000..98745f4dc --- /dev/null +++ b/src/bin/common/file_utils.h @@ -0,0 +1,73 @@ +/* + * src/bin/pg_autoctl/file_utils.h + * Utility functions for reading and writing files + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the PostgreSQL License. + * + */ + +#ifndef FILE_UTILS_H +#define FILE_UTILS_H + +#include + +#include "postgres_fe.h" + +#include + + +#if defined(__APPLE__) +#define ST_MTIME_S(st) ((int64_t) st.st_mtimespec.tv_sec) +#else +#define ST_MTIME_S(st) ((int64_t) st.st_mtime) +#endif + + +/* + * In order to avoid dynamic memory allocations and tracking when searching the + * PATH environment, we pre-allocate 1024 paths entries. That should be way + * more than enough for all situations, and only costs 1024*1024 = 1MB of + * memory. + */ +typedef struct SearchPath +{ + int found; + char matches[1024][MAXPGPATH]; +} SearchPath; + + +bool file_exists(const char *filename); +bool directory_exists(const char *path); +bool ensure_empty_dir(const char *dirname, int mode); +FILE * fopen_with_umask(const char *filePath, const char *modes, int flags, mode_t umask); +FILE * fopen_read_only(const char *filePath); +bool write_file(char *data, long fileSize, const char *filePath); +bool append_to_file(char *data, long fileSize, const char *filePath); +bool read_file(const char *filePath, char **contents, long *fileSize); +bool read_file_if_exists(const char *filePath, char **contents, long *fileSize); +bool move_file(char *sourcePath, char *destinationPath); +bool duplicate_file(char *sourcePath, char *destinationPath); +bool create_symbolic_link(char *sourcePath, char *targetPath); + +void path_in_same_directory(const char *basePath, + const char *fileName, + char *destinationPath); + +bool search_path_first(const char *filename, char *result, int logLevel); +bool search_path(const char *filename, SearchPath *result); +bool search_path_deduplicate_symlinks(SearchPath *results, SearchPath *dedup); +bool unlink_file(const char *filename); +bool set_program_absolute_path(char *program, int size); +bool normalize_filename(const char *filename, char *dst, int size); + +void init_ps_buffer(int argc, char **argv); +void set_ps_title(const char *title); + +int fformat(FILE *stream, const char *fmt, ...) +__attribute__((format(printf, 2, 3))); + +int sformat(char *str, size_t count, const char *fmt, ...) +__attribute__((format(printf, 3, 4))); + +#endif /* FILE_UTILS_H */ diff --git a/src/bin/common/ini_file.c b/src/bin/common/ini_file.c new file mode 100644 index 000000000..309f77d57 --- /dev/null +++ b/src/bin/common/ini_file.c @@ -0,0 +1,743 @@ +/* + * src/bin/pg_autoctl/ini_file.c + * Functions to parse a configuration file using the .INI syntax. + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the PostgreSQL License. + * + */ + +#include +#include + +#include "ini.h" +#include "ini_file.h" +#include "log.h" +#include "pgctl.h" +#include "parson.h" +#include "pgsetup.h" +#include "string_utils.h" + +/* + * Load a configuration file in the INI format. + */ +bool +read_ini_file(const char *filename, IniOption *optionList) +{ + char *fileContents = NULL; + long fileSize = 0L; + + /* read the current postgresql.conf contents */ + if (!read_file(filename, &fileContents, &fileSize)) + { + return false; + } + + return parse_ini_buffer(filename, fileContents, optionList); +} + + +/* + * parse_ini_buffer parses the content of a config.ini file. + */ +bool +parse_ini_buffer(const char *filename, + char *fileContents, + IniOption *optionList) +{ + IniOption *option; + + /* parse the content of the file as per INI syntax rules */ + ini_t *ini = ini_load(fileContents, NULL); + free(fileContents); + + /* + * Now that the INI file is loaded into a generic structure, run through it + * to find given opts and set. + */ + for (option = optionList; option->type != INI_END_T; option++) + { + int optionIndex; + char *val; + + int sectionIndex = ini_find_section(ini, option->section, 0); + + if (sectionIndex == INI_NOT_FOUND) + { + if (option->required) + { + log_error("Failed to find section %s in \"%s\"", + option->section, filename); + ini_destroy(ini); + return false; + } + optionIndex = INI_NOT_FOUND; + } + else + { + optionIndex = ini_find_property(ini, sectionIndex, option->name, 0); + } + + /* + * When we didn't find an option, we have three cases to consider: + * 1. it's required, error out + * 2. it's a compatibility option, skip it + * 3. use the default value instead + */ + if (optionIndex == INI_NOT_FOUND) + { + if (option->required) + { + log_error("Failed to find option %s.%s in \"%s\"", + option->section, option->name, filename); + ini_destroy(ini); + return false; + } + else if (option->compat) + { + /* skip compatibility options that are not found */ + continue; + } + else + { + switch (option->type) + { + case INI_INT_T: + { + *(option->intValue) = option->intDefault; + break; + } + + case INI_STRING_T: + case INI_STRBUF_T: + { + ini_set_option_value(option, option->strDefault); + break; + } + + default: + { + /* should never happen, or it's a development bug */ + log_fatal("Unknown option type %d", option->type); + ini_destroy(ini); + return false; + } + } + } + } + else + { + val = (char *) ini_property_value(ini, sectionIndex, optionIndex); + + log_trace("%s.%s = %s", option->section, option->name, val); + + if (val != NULL) + { + if (!ini_set_option_value(option, val)) + { + /* we logged about it already */ + ini_destroy(ini); + return false; + } + } + } + } + ini_destroy(ini); + return true; +} + + +/* + * ini_validate_options walks through an optionList and installs default values + * when necessary, and returns false if any required option is missing and + * doesn't have a default provided. + */ +bool +ini_validate_options(IniOption *optionList) +{ + IniOption *option; + + for (option = optionList; option->type != INI_END_T; option++) + { + char optionName[BUFSIZE]; + + int n = sformat(optionName, BUFSIZE, "%s.%s", option->section, option->name); + + if (option->optName) + { + sformat(optionName + n, BUFSIZE - n, " (--%s)", option->optName); + } + + switch (option->type) + { + case INI_INT_T: + { + if (*(option->intValue) == -1 && option->intDefault != -1) + { + *(option->intValue) = option->intDefault; + } + + if (option->required && *(option->intValue) == -1) + { + log_error("Option %s is required and has not been set", + optionName); + return false; + } + break; + } + + case INI_STRING_T: + { + if (*(option->strValue) == NULL && option->strDefault != NULL) + { + ini_set_option_value(option, option->strDefault); + } + + if (option->required && *(option->strValue) == NULL) + { + log_error("Option %ss is required and has not been set", + optionName); + return false; + } + break; + } + + case INI_STRBUF_T: + { + if (IS_EMPTY_STRING_BUFFER(option->strBufValue) && + option->strDefault != NULL) + { + ini_set_option_value(option, option->strDefault); + } + + if (option->required && IS_EMPTY_STRING_BUFFER(option->strBufValue)) + { + log_error("Option %s is required and has not been set", + optionName); + return false; + } + break; + } + + default: + { + /* should never happen, or it's a development bug */ + log_fatal("Unknown option type %d", option->type); + return false; + } + } + } + return true; +} + + +/* + * ini_set_option_value saves given value to option, parsing the value string + * as its type require. + */ +bool +ini_set_option_value(IniOption *option, const char *value) +{ + if (option == NULL) + { + return false; + } + + switch (option->type) + { + case INI_STRING_T: + { + if (value == NULL) + { + *(option->strValue) = NULL; + } + else + { + *(option->strValue) = strdup(value); + if (*(option->strValue) == NULL) + { + log_error(ALLOCATION_FAILED_ERROR); + return false; + } + } + break; + } + + case INI_STRBUF_T: + { + /* + * When given a String Buffer str[SIZE], then we are given strbuf + * as the address where to host the data directly. + */ + if (value == NULL) + { + /* null are handled as bytes of '\0' in string buffers */ + bzero((void *) option->strBufValue, option->strBufferSize); + } + else + { + strlcpy((char *) option->strBufValue, value, option->strBufferSize); + } + break; + } + + case INI_INT_T: + { + if (value) + { + int nb; + + if (!stringToInt(value, &nb)) + { + log_error("Failed to parse %s.%s's value \"%s\" as a number", + option->section, option->name, value); + return false; + } + *(option->intValue) = nb; + } + break; + } + + default: + { + /* developer error, should never happen */ + log_fatal("Unknown option type %d", option->type); + return false; + } + } + return true; +} + + +/* + * Format a single option as a string value. + */ +bool +ini_option_to_string(IniOption *option, char *dest, size_t size) +{ + switch (option->type) + { + case INI_STRING_T: + { + /* option->strValue is a char **, both pointers could be NULL */ + if (option->strValue == NULL || *(option->strValue) == NULL) + { + return false; + } + + strlcpy(dest, *(option->strValue), size); + return true; + } + + case INI_STRBUF_T: + { + strlcpy(dest, (char *) option->strBufValue, size); + return true; + } + + case INI_INT_T: + { + sformat(dest, size, "%d", *(option->intValue)); + return true; + } + + default: + { + log_fatal("Unknown option type %d", option->type); + return false; + } + } + return false; +} + + +#define streq(x, y) ((x != NULL) && (y != NULL) && ( \ + strcmp(x, y) == 0)) + +/* + * write_ini_to_stream writes in-memory INI structure to given STREAM in the + * INI format specifications. + */ +bool +write_ini_to_stream(FILE *stream, IniOption *optionList) +{ + char *currentSection = NULL; + IniOption *option; + + for (option = optionList; option->type != INI_END_T; option++) + { + /* we read "compatibility" options but never write them back */ + if (option->compat) + { + continue; + } + + /* we might need to open a new section */ + if (!streq(currentSection, option->section)) + { + if (currentSection != NULL) + { + fformat(stream, "\n"); + } + currentSection = (char *) option->section; + fformat(stream, "[%s]\n", currentSection); + } + + switch (option->type) + { + case INI_INT_T: + { + fformat(stream, "%s = %d\n", + option->name, *(option->intValue)); + break; + } + + case INI_STRING_T: + { + char *value = *(option->strValue); + + if (value) + { + fformat(stream, "%s = %s\n", option->name, value); + } + else if (option->required) + { + log_error("Option %s.%s is required but is not set", + option->section, option->name); + return false; + } + break; + } + + case INI_STRBUF_T: + { + /* here we have a string buffer, which is its own address */ + char *value = (char *) option->strBufValue; + + if (value[0] != '\0') + { + fformat(stream, "%s = %s\n", option->name, value); + } + else if (option->required) + { + log_error("Option %s.%s is required but is not set", + option->section, option->name); + return false; + } + break; + } + + default: + { + /* developper error, should never happen */ + log_fatal("Unknown option type %d", option->type); + break; + } + } + } + fflush(stream); + return true; +} + + +/* + * ini_to_json populates the given JSON value with the contents of the INI + * file. Sections become JSON objects, options the keys to the section objects. + */ +bool +ini_to_json(JSON_Object *jsRoot, IniOption *optionList) +{ + char *currentSection = NULL; + JSON_Value *currentSectionJs = NULL; + JSON_Object *currentSectionJsObj = NULL; + IniOption *option = NULL; + + for (option = optionList; option->type != INI_END_T; option++) + { + /* we read "compatibility" options but never write them back */ + if (option->compat) + { + continue; + } + + /* we might need to open a new section */ + if (!streq(currentSection, option->section)) + { + if (currentSection != NULL) + { + json_object_set_value(jsRoot, currentSection, currentSectionJs); + } + + currentSectionJs = json_value_init_object(); + currentSectionJsObj = json_value_get_object(currentSectionJs); + + currentSection = (char *) option->section; + } + + switch (option->type) + { + case INI_INT_T: + { + json_object_set_number(currentSectionJsObj, + option->name, + (double) *(option->intValue)); + break; + } + + case INI_STRING_T: + { + char *value = *(option->strValue); + + if (value) + { + json_object_set_string(currentSectionJsObj, + option->name, + value); + } + else if (option->required) + { + log_error("Option %s.%s is required but is not set", + option->section, option->name); + return false; + } + break; + } + + case INI_STRBUF_T: + { + /* here we have a string buffer, which is its own address */ + char *value = (char *) option->strBufValue; + + if (value[0] != '\0') + { + json_object_set_string(currentSectionJsObj, + option->name, + value); + } + else if (option->required) + { + log_error("Option %s.%s is required but is not set", + option->section, option->name); + return false; + } + break; + } + + default: + { + /* developper error, should never happen */ + log_fatal("Unknown option type %d", option->type); + break; + } + } + } + + if (currentSection != NULL) + { + json_object_set_value(jsRoot, currentSection, currentSectionJs); + } + + return true; +} + + +/* + * lookup_ini_option implements an option lookup given a section name and an + * option name. + */ +IniOption * +lookup_ini_option(IniOption *optionList, const char *section, const char *name) +{ + IniOption *option; + + /* now lookup section/option names in opts */ + for (option = optionList; option->type != INI_END_T; option++) + { + if (streq(option->section, section) && streq(option->name, name)) + { + return option; + } + } + return NULL; +} + + +/* + * Lookup an option value given a "path" of section.option. + */ +IniOption * +lookup_ini_path_value(IniOption *optionList, const char *path) +{ + char *section_name, *option_name, *ptr; + + /* + * Split path into section/option. + */ + ptr = strchr(path, '.'); + + if (ptr == NULL) + { + log_error("Failed to find a dot separator in option path \"%s\"", path); + return NULL; + } + + section_name = strdup(path); /* don't scribble on path */ + if (section_name == NULL) + { + log_error(ALLOCATION_FAILED_ERROR); + return NULL; + } + option_name = section_name + (ptr - path) + 1; /* apply same offset */ + *(option_name - 1) = '\0'; /* split string at the dot */ + + IniOption *option = lookup_ini_option(optionList, section_name, option_name); + + if (option == NULL) + { + log_error("Failed to find configuration option for path \"%s\"", path); + } + + free(section_name); + + return option; +} + + +/* + * ini_merge merges the options that have been set in overrideOptionList into + * the options in dstOptionList, ignoring default values. + */ +bool +ini_merge(IniOption *dstOptionList, IniOption *overrideOptionList) +{ + IniOption *option; + + for (option = overrideOptionList; option->type != INI_END_T; option++) + { + IniOption *dstOption = + lookup_ini_option(dstOptionList, option->section, option->name); + + if (dstOption == NULL) + { + /* developper error, why do we have incompatible INI options? */ + log_error("BUG: ini_merge: lookup failed in dstOptionList(%s, %s)", + option->section, option->name); + return false; + } + + switch (option->type) + { + case INI_INT_T: + { + if (*(option->intValue) != -1 && *(option->intValue) != 0) + { + *(dstOption->intValue) = *(option->intValue); + } + break; + } + + case INI_STRING_T: + { + if (*(option->strValue) != NULL) + { + *(dstOption->strValue) = strdup(*(option->strValue)); + if (*(dstOption->strValue) == NULL) + { + log_error(ALLOCATION_FAILED_ERROR); + return false; + } + } + break; + } + + case INI_STRBUF_T: + { + if (!IS_EMPTY_STRING_BUFFER(option->strBufValue)) + { + strlcpy((char *) dstOption->strBufValue, + (char *) option->strBufValue, + dstOption->strBufferSize); + } + break; + } + + default: + { + /* should never happen, or it's a development bug */ + log_fatal("Unknown option type %d", option->type); + return false; + } + } + } + return true; +} + + +/* + * ini_get_setting reads given INI filename and maps its content using an + * optionList that instructs which options to read and what default values to + * use. Then ini_get_setting looks up the given path (section.option) and sets + * the given value string. + */ +bool +ini_get_setting(const char *filename, IniOption *optionList, + const char *path, char *value, size_t size) +{ + log_debug("Reading configuration from \"%s\"", filename); + + if (!read_ini_file(filename, optionList)) + { + log_error("Failed to parse configuration file \"%s\"", filename); + return false; + } + + IniOption *option = lookup_ini_path_value(optionList, path); + + if (option) + { + return ini_option_to_string(option, value, size); + } + + return false; +} + + +/* + * ini_set_option sets the INI value to the given value. + */ +bool +ini_set_option(IniOption *optionList, const char *path, char *value) +{ + IniOption *option = lookup_ini_path_value(optionList, path); + + if (option && ini_set_option_value(option, value)) + { + log_debug("ini_set_option %s.%s = %s", + option->section, option->name, value); + + return true; + } + + return false; +} + + +/* + * ini_set_setting sets the INI filename option identified by path to the given + * value. optionList is used to know how to read the values in the file and + * also contains the default values. + */ +bool +ini_set_setting(const char *filename, IniOption *optionList, + const char *path, char *value) +{ + log_debug("Reading configuration from %s", filename); + + if (!read_ini_file(filename, optionList)) + { + log_error("Failed to parse configuration file \"%s\"", filename); + return false; + } + + return ini_set_option(optionList, path, value); +} diff --git a/src/bin/common/ini_file.h b/src/bin/common/ini_file.h new file mode 100644 index 000000000..7e3d61918 --- /dev/null +++ b/src/bin/common/ini_file.h @@ -0,0 +1,117 @@ +/* + * src/bin/pg_autoctl/ini_file.h + * Functions to parse a configuration file using the .INI syntax. + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the PostgreSQL License. + * + */ + +#ifndef INI_FILE_H +#define INI_FILE_H + +#include +#include + +#include "parson.h" + +#define INI_STRING_T 1 /* char *target */ +#define INI_STRBUF_T 2 /* char target[size] */ +#define INI_INT_T 3 /* int target */ +#define INI_END_T 4 + +/* + * IniOption represent a key/value as written in the INI format: + * + * [section] + * name = "values" + * int = 10 + * + * The IniOption structure is used both for specifying what we expect to read + * in the INI file: required, strdefault, and intdefault, and what has been + * actually read from it: strval/intval. + * + * Given the previous contents and this structure as input: + * + * { + * {INI_STRING_T, "section", "name", true, "default", -1, -1, &str, NULL}, + * {INI_INT_T, "section", "int", true, NULL, 1, -1, NULL, &int}, + * {INI_END_T, NULL, NULL, false, NULL, -1, -1, NULL, NULL} + * } + * + * Then after reading the ini file with `read_ini_file' then *str = "values" + * and *int = 10. + */ +typedef struct IniOption +{ + int type; + const char *section; + const char *name; + const char *optName; /* command line option name */ + bool required; + bool compat; /* compatibility: read but don't write */ + char *strDefault; /* default value when type is string */ + int intDefault; /* default value when type is int */ + int strBufferSize; /* size of the BUFFER when INI_STRBUF_T */ + char **strValue; /* pointer to a string pointer (typically malloc-ed) */ + char *strBufValue; /* pointer to a string buffer (on the stack) */ + int *intValue; /* pointer to an integer */ +} IniOption; + +#define make_int_option(section, name, optName, required, value) \ + { INI_INT_T, section, name, optName, required, false, \ + NULL, -1, -1, NULL, NULL, value } + +#define make_int_option_default(section, name, optName, \ + required, value, default) \ + { INI_INT_T, section, name, optName, required, false, \ + NULL, default, -1, NULL, NULL, value } + +#define make_string_option(section, name, optName, required, value) \ + { INI_STRING_T, section, name, optName, required, false, \ + NULL, -1, -1, value, NULL, NULL } + +#define make_string_option_default(section, name, optName, required, \ + value, default) \ + { INI_STRING_T, section, name, optName, required, false, \ + default, -1, -1, value, NULL, NULL } + +#define make_strbuf_option(section, name, optName, required, size, value) \ + { INI_STRBUF_T, section, name, optName, required, false, \ + NULL, -1, size, NULL, value, NULL } + +#define make_strbuf_compat_option(section, name, size, value) \ + { INI_STRBUF_T, section, name, NULL, false, true, \ + NULL, -1, size, NULL, value, NULL } + +#define make_strbuf_option_default(section, name, optName, required, \ + size, value, default) \ + { INI_STRBUF_T, section, name, optName, required, false, \ + default, -1, size, NULL, value, NULL } + +#define INI_OPTION_LAST \ + { INI_END_T, NULL, NULL, NULL, false, false, NULL, -1, -1, NULL, NULL, NULL } + +bool read_ini_file(const char *filename, IniOption *opts); +bool parse_ini_buffer(const char *filename, + char *fileContents, + IniOption *optionList); +bool ini_validate_options(IniOption *optionList); +bool ini_set_option_value(IniOption *option, const char *value); +bool ini_option_to_string(IniOption *option, char *dest, size_t size); +bool write_ini_to_stream(FILE *stream, IniOption *optionList); +bool ini_to_json(JSON_Object *jsRoot, IniOption *optionList); +IniOption * lookup_ini_option(IniOption *optionList, + const char *section, const char *name); +IniOption * lookup_ini_path_value(IniOption *optionList, const char *path); +bool ini_merge(IniOption *dstOptionList, IniOption *overrideOptionList); + +bool ini_set_option(IniOption *optionList, const char *path, char *value); + +bool ini_get_setting(const char *filename, IniOption *optionList, + const char *path, char *value, size_t size); +bool ini_set_setting(const char *filename, IniOption *optionList, + const char *path, char *value); + + +#endif /* INI_FILE_H */ diff --git a/src/bin/common/ini_implementation.c b/src/bin/common/ini_implementation.c new file mode 100644 index 000000000..963095207 --- /dev/null +++ b/src/bin/common/ini_implementation.c @@ -0,0 +1,13 @@ +/* + * src/bin/pg_autoctl/ini_implementation.c + * The file containing library code used to parse files with .INI syntax + * + * The main reason this is in a separate file is so you can exclude a file + * during static analysis. This way we exclude vendored in library code, + * but not our code using it. + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the PostgreSQL License. + */ +#define INI_IMPLEMENTATION +#include "ini.h" diff --git a/src/bin/common/ipaddr.c b/src/bin/common/ipaddr.c new file mode 100644 index 000000000..b1d78ef28 --- /dev/null +++ b/src/bin/common/ipaddr.c @@ -0,0 +1,922 @@ +/* + * src/bin/pg_autoctl/ipaddr.c + * Find local ip used as source ip in ip packets, using getsockname and a udp + * connection. + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the PostgreSQL License. + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "postgres_fe.h" + +#include "defaults.h" +#include "env_utils.h" +#include "file_utils.h" +#include "ipaddr.h" +#include "log.h" +#include "pgsetup.h" +#include "pgsql.h" +#include "string_utils.h" + +static unsigned int countSetBits(unsigned int n); +static unsigned int countSetBitsv6(unsigned char *addr); +static bool ipv4eq(struct sockaddr_in *a, struct sockaddr_in *b); +static bool ipv6eq(struct sockaddr_in6 *a, struct sockaddr_in6 *b); +static bool fetchIPAddressFromInterfaceList(char *localIpAddress, int size); +static bool ipaddr_sockaddr_to_string(struct addrinfo *ai, + char *ipaddr, size_t size); +static bool ipaddr_getsockname(int sock, char *ipaddr, size_t size); +static bool GetAddrInfo(const char *restrict node, + const char *restrict service, + const struct addrinfo *restrict hints, + struct addrinfo **restrict res); + + +/* + * Connect to given serviceName and servicePort in TCP in order to determine + * which local IP address has been used to connect. That local IP address is + * then the one we use for the default --hostname value, when not provided. + * + * On a keeper, we use the monitor hostname as the serviceName. On the monitor, + * we use DEFAULT_INTERFACE_LOOKUP_SERVICE_NAME to discover the local default + * outbound IP address. + */ +bool +fetchLocalIPAddress(char *localIpAddress, int size, + const char *serviceName, int servicePort, + int logLevel, bool *mayRetry) +{ + struct addrinfo *lookup; + struct addrinfo *ai; + struct addrinfo hints; + + bool couldConnect = false; + + int sock; + + *mayRetry = false; + + /* prepare getaddrinfo hints for name resolution or IP address parsing */ + memset(&hints, 0, sizeof(hints)); + hints.ai_family = PF_UNSPEC; /* accept any family as supported by OS */ + hints.ai_socktype = SOCK_STREAM; /* we only want TCP sockets */ + hints.ai_protocol = IPPROTO_TCP; /* we only want TCP sockets */ + + if (!GetAddrInfo(serviceName, + intToString(servicePort).strValue, + &hints, + &lookup)) + { + /* errors have already been logged */ + return false; + } + + for (ai = lookup; ai; ai = ai->ai_next) + { + char addr[BUFSIZE] = { 0 }; + + if (!ipaddr_sockaddr_to_string(ai, addr, sizeof(addr))) + { + /* errors have already been logged */ + return false; + } + + sock = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol); + + if (sock < 0) + { + log_warn("Failed to create a socket: %m"); + return false; + } + + /* connect timeout can be quite long by default */ + log_info("Connecting to %s (port %d)", addr, servicePort); + + int err = connect(sock, ai->ai_addr, ai->ai_addrlen); + + if (err < 0) + { + log_level(logLevel, "Failed to connect to %s: %m", addr); + } + else + { + /* found a getaddrinfo() result we could use to connect */ + couldConnect = true; + break; + } + } + + freeaddrinfo(lookup); + + if (!couldConnect) + { + if (env_found_empty("PG_REGRESS_SOCK_DIR")) + { + /* + * In test environment, in case of no internet access, just use the + * address of the non-loopback network interface. + */ + return fetchIPAddressFromInterfaceList(localIpAddress, size); + } + else + { + *mayRetry = true; + + if (strcmp(DEFAULT_INTERFACE_LOOKUP_SERVICE_NAME, serviceName) == 0) + { + log_level(logLevel, + "Failed to connect to \"%s\" on port %d " + "to discover this machine hostname, " + "please use --hostname", + serviceName, servicePort); + } + else + { + log_level(logLevel, + "Failed to connect to any of the IP addresses for " + "monitor hostname \"%s\" and port %d", + serviceName, servicePort); + } + return false; + } + } + + if (!ipaddr_getsockname(sock, localIpAddress, size)) + { + /* errors have already been logged */ + close(sock); + return false; + } + + close(sock); + + return true; +} + + +/* + * fetchLocalCIDR loops over the local interfaces on the host and finds the one + * for which the IP address is the same as the given localIpAddress parameter. + * Then using the netmask information from the network interface, + * fetchLocalCIDR computes the local CIDR to use in HBA in order to allow + * authentication of all servers in the local network. + */ +bool +fetchLocalCIDR(const char *localIpAddress, char *localCIDR, int size) +{ + char network[INET6_ADDRSTRLEN]; + struct ifaddrs *ifaddr, *ifa; + int prefix = 0; + bool found = false; + + if (getifaddrs(&ifaddr) == -1) + { + log_warn("Failed to get the list of local network inferfaces: %m"); + return false; + } + + for (ifa = ifaddr; ifa; ifa = ifa->ifa_next) + { + char netmask[INET6_ADDRSTRLEN] = { 0 }; + char address[INET6_ADDRSTRLEN] = { 0 }; + + /* + * Some interfaces might have an empty ifa_addr, such as when using the + * PPTP protocol. With a NULL ifa_addr we can't inquire about the IP + * address and its netmask to compute any CIDR notation, so we skip the + * entry. + */ + if (ifa->ifa_addr == NULL) + { + log_debug("Skipping interface \"%s\" with NULL ifa_addr", + ifa->ifa_name); + continue; + } + + switch (ifa->ifa_addr->sa_family) + { + case AF_INET: + { + struct sockaddr_in *netmask4 = + (struct sockaddr_in *) ifa->ifa_netmask; + struct sockaddr_in *address4 = + (struct sockaddr_in *) ifa->ifa_addr; + + struct in_addr s_network; + + if (inet_ntop(AF_INET, (void *) &netmask4->sin_addr, + netmask, INET_ADDRSTRLEN) == NULL) + { + /* just skip that entry then */ + log_trace("Failed to determine local network CIDR: %m"); + continue; + } + + if (inet_ntop(AF_INET, (void *) &address4->sin_addr, + address, INET_ADDRSTRLEN) == NULL) + { + /* just skip that entry then */ + log_trace("Failed to determine local network CIDR: %m"); + continue; + } + + + s_network.s_addr = + address4->sin_addr.s_addr & netmask4->sin_addr.s_addr; + + prefix = countSetBits(netmask4->sin_addr.s_addr); + + if (inet_ntop(AF_INET, (void *) &s_network, + network, INET_ADDRSTRLEN) == NULL) + { + /* just skip that entry then */ + log_trace("Failed to determine local network CIDR: %m"); + continue; + } + + break; + } + + case AF_INET6: + { + int i = 0; + struct sockaddr_in6 *netmask6 = + (struct sockaddr_in6 *) ifa->ifa_netmask; + struct sockaddr_in6 *address6 = + (struct sockaddr_in6 *) ifa->ifa_addr; + + struct in6_addr s_network; + + if (inet_ntop(AF_INET6, (void *) &netmask6->sin6_addr, + netmask, INET6_ADDRSTRLEN) == NULL) + { + /* just skip that entry then */ + log_trace("Failed to determine local network CIDR: %m"); + continue; + } + + if (inet_ntop(AF_INET6, (void *) &address6->sin6_addr, + address, INET6_ADDRSTRLEN) == NULL) + { + /* just skip that entry then */ + log_trace("Failed to determine local network CIDR: %m"); + continue; + } + + for (i = 0; i < sizeof(struct in6_addr); i++) + { + s_network.s6_addr[i] = + address6->sin6_addr.s6_addr[i] & + netmask6->sin6_addr.s6_addr[i]; + } + + prefix = countSetBitsv6(netmask6->sin6_addr.s6_addr); + + if (inet_ntop(AF_INET6, &s_network, + network, INET6_ADDRSTRLEN) == NULL) + { + /* just skip that entry then */ + log_trace("Failed to determine local network CIDR: %m"); + continue; + } + + break; + } + + default: + { + continue; + } + } + + if (strcmp(address, localIpAddress) == 0) + { + found = true; + break; + } + } + freeifaddrs(ifaddr); + + if (!found) + { + return false; + } + + sformat(localCIDR, size, "%s/%d", network, prefix); + + return true; +} + + +/* + * countSetBits return how many bits are set (to 1) in an integer. When given a + * netmask, that's the CIDR prefix. + */ +static unsigned int +countSetBits(unsigned int n) +{ + unsigned int count = 0; + + while (n) + { + count += n & 1; + n >>= 1; + } + + return count; +} + + +/* + * countSetBitsv6 returns how many bits are set (to 1) in an IPv6 address, an + * array of 16 unsigned char values. When given a netmask, that's the + * prefixlen. + */ +static unsigned int +countSetBitsv6(unsigned char *addr) +{ + int i = 0; + unsigned int count = 0; + + for (i = 0; i < 16; i++) + { + unsigned char n = addr[i]; + + while (n) + { + count += n & 1; + n >>= 1; + } + } + + return count; +} + + +/* + * Fetches the IP address of the first non-loopback interface with an ip4 + * address. + */ +static bool +fetchIPAddressFromInterfaceList(char *localIpAddress, int size) +{ + bool found = false; + struct ifaddrs *ifaddrList = NULL, *ifaddr = NULL; + + if (getifaddrs(&ifaddr) == -1) + { + log_error("Failed to get the list of local network inferfaces: %m"); + return false; + } + + for (ifaddr = ifaddrList; ifaddr != NULL; ifaddr = ifaddr->ifa_next) + { + if (ifaddr->ifa_flags & IFF_LOOPBACK) + { + log_trace("Skipping loopback interface \"%s\"", ifaddr->ifa_name); + continue; + } + + /* + * Some interfaces might have an empty ifa_addr, such as when using the + * PPTP protocol. With a NULL ifa_addr we can't inquire about the IP + * address and its netmask to compute any CIDR notation, so we skip the + * entry. + */ + if (ifaddr->ifa_addr == NULL) + { + log_debug("Skipping interface \"%s\" with NULL ifa_addr", + ifaddr->ifa_name); + continue; + } + + /* + * We only support IPv4 here, also this function is only called in test + * environment where we run in a docker container with a network + * namespace in which we use only IPv4, so that's ok. + */ + if (ifaddr->ifa_addr->sa_family == AF_INET) + { + struct sockaddr_in *ip = + (struct sockaddr_in *) ifaddr->ifa_addr; + + if (inet_ntop(AF_INET, (void *) &(ip->sin_addr), + localIpAddress, size) == NULL) + { + /* skip that address, silently */ + log_trace("Failed to determine local network CIDR: %m"); + continue; + } + + found = true; + break; + } + } + + freeifaddrs(ifaddrList); + + return found; +} + + +/* + * From /Users/dim/dev/PostgreSQL/postgresql/src/backend/libpq/hba.c + */ +static bool +ipv4eq(struct sockaddr_in *a, struct sockaddr_in *b) +{ + return (a->sin_addr.s_addr == b->sin_addr.s_addr); +} + + +/* + * From /Users/dim/dev/PostgreSQL/postgresql/src/backend/libpq/hba.c + */ +static bool +ipv6eq(struct sockaddr_in6 *a, struct sockaddr_in6 *b) +{ + int i; + + for (i = 0; i < 16; i++) + { + if (a->sin6_addr.s6_addr[i] != b->sin6_addr.s6_addr[i]) + { + return false; + } + } + + return true; +} + + +/* + * findHostnameLocalAddress does a reverse DNS lookup given a hostname + * (--hostname), and if the DNS lookup fails or doesn't return any local IP + * address, then returns false. + */ +bool +findHostnameLocalAddress(const char *hostname, char *localIpAddress, int size) +{ + struct addrinfo *dns_lookup_addr; + struct addrinfo *dns_addr; + struct ifaddrs *ifaddrList, *ifaddr; + + if (!GetAddrInfo(hostname, NULL, 0, &dns_lookup_addr)) + { + /* errors have already been logged */ + return false; + } + + /* + * Loop over DNS results for the given hostname. Filter out loopback + * devices, and for each IP address given by the look-up, check if we + * have a corresponding local interface bound to the IP address. + */ + if (getifaddrs(&ifaddrList) == -1) + { + log_warn("Failed to get the list of local network inferfaces: %m"); + return false; + } + + /* + * Compare both addresses list (dns lookup and list of interface + * addresses) in a nested loop fashion: lists are not sorted, and we + * expect something like a dozen entry per list anyway. + */ + for (dns_addr = dns_lookup_addr; + dns_addr != NULL; + dns_addr = dns_addr->ai_next) + { + for (ifaddr = ifaddrList; ifaddr != NULL; ifaddr = ifaddr->ifa_next) + { + /* + * Some interfaces might have an empty ifa_addr, such as when using + * the PPTP protocol. With a NULL ifa_addr we can't inquire about + * the IP address and its netmask to compute any CIDR notation, so + * we skip the entry. + */ + if (ifaddr->ifa_addr == NULL) + { + log_debug("Skipping interface \"%s\" with NULL ifa_addr", + ifaddr->ifa_name); + continue; + } + + if (ifaddr->ifa_addr->sa_family == AF_INET && + dns_addr->ai_family == AF_INET) + { + struct sockaddr_in *ip = + (struct sockaddr_in *) ifaddr->ifa_addr; + + if (ipv4eq(ip, (struct sockaddr_in *) dns_addr->ai_addr)) + { + /* + * Found an IP address in the DNS answer that + * matches one of the interfaces IP addresses on + * the machine. + */ + freeaddrinfo(dns_lookup_addr); + + if (inet_ntop(AF_INET, + (void *) &(ip->sin_addr), + localIpAddress, + size) == NULL) + { + log_warn("Failed to determine local ip address: %m"); + freeifaddrs(ifaddrList); + return false; + } + + freeifaddrs(ifaddrList); + return true; + } + } + else if (ifaddr->ifa_addr->sa_family == AF_INET6 && + dns_addr->ai_family == AF_INET6) + { + struct sockaddr_in6 *ip = + (struct sockaddr_in6 *) ifaddr->ifa_addr; + + if (ipv6eq(ip, (struct sockaddr_in6 *) dns_addr->ai_addr)) + { + /* + * Found an IP address in the DNS answer that + * matches one of the interfaces IP addresses on + * the machine. + */ + freeaddrinfo(dns_lookup_addr); + + if (inet_ntop(AF_INET6, + (void *) &(ip->sin6_addr), + localIpAddress, + size) == NULL) + { + /* check size >= INET6_ADDRSTRLEN */ + log_warn("Failed to determine local ip address: %m"); + freeifaddrs(ifaddrList); + return false; + } + + freeifaddrs(ifaddrList); + return true; + } + } + } + } + + freeifaddrs(ifaddrList); + freeaddrinfo(dns_lookup_addr); + return false; +} + + +/* + * ip_address_type parses the hostname and determines whether it is an IPv4 + * address, IPv6 address, or DNS name. + * + * To edit pg HBA file, when given an IP address (rather than a hostname), we + * need to compute the CIDR mask. In the case of ipv4, that's /32, in the case + * of ipv6, that's /128. The `ip_address_type' function discovers which type of + * IP address we are dealing with. + */ +IPType +ip_address_type(const char *hostname) +{ + struct in_addr ipv4; + struct in6_addr ipv6; + + if (hostname == NULL) + { + return IPTYPE_NONE; + } + else if (inet_pton(AF_INET, hostname, &ipv4) == 1) + { + log_trace("hostname \"%s\" is ipv4", hostname); + return IPTYPE_V4; + } + else if (inet_pton(AF_INET6, hostname, &ipv6) == 1) + { + log_trace("hostname \"%s\" is ipv6", hostname); + return IPTYPE_V6; + } + return IPTYPE_NONE; +} + + +/* + * findHostnameFromLocalIpAddress does a reverse DNS lookup from a given IP + * address, and returns the first hostname of the DNS response. + */ +bool +findHostnameFromLocalIpAddress(char *localIpAddress, char *hostname, int size) +{ + char hbuf[NI_MAXHOST]; + struct addrinfo *lookup, *ai; + + /* parse ipv4 or ipv6 address using getaddrinfo() */ + if (!GetAddrInfo(localIpAddress, NULL, 0, &lookup)) + { + /* errors have already been logged */ + return false; + } + + /* now reverse lookup (NI_NAMEREQD) the address with getnameinfo() */ + for (ai = lookup; ai; ai = ai->ai_next) + { + int ret = getnameinfo(ai->ai_addr, ai->ai_addrlen, + hbuf, sizeof(hbuf), NULL, 0, NI_NAMEREQD); + + if (ret != 0) + { + log_warn("Failed to resolve hostname from address \"%s\": %s", + localIpAddress, gai_strerror(ret)); + return false; + } + + sformat(hostname, size, "%s", hbuf); + + /* stop at the first hostname found */ + break; + } + freeaddrinfo(lookup); + + return true; +} + + +/* + * resolveHostnameForwardAndReverse returns true when we could do a forward DNS + * lookup for the hostname and one of the IP addresses from the lookup resolves + * back to the hostname when doing a reverse-DNS lookup from it. + * + * When Postgres runs the DNS checks in the HBA implementation, the client IP + * address is looked-up in a reverse DNS query, and that name is compared to + * the hostname in the HBA file. Then, a forward DNS query is performed on the + * hostname, and one of the IP addresses returned must match with the client IP + * address. + * + * client ip -- reverse dns lookup --> hostname + * hostname -- forward dns lookup --> { ... client ip ... } + * + * At this point we don't have a client IP address. That said, the Postgres + * check will always fail if we fail to get our hostname back from at least one + * of the IP addresses that our hostname forward-DNS query returns. + */ +bool +resolveHostnameForwardAndReverse(const char *hostname, char *ipaddr, int size, + bool *foundHostnameFromAddress) +{ + struct addrinfo *lookup, *ai; + + *foundHostnameFromAddress = false; + + if (!GetAddrInfo(hostname, NULL, 0, &lookup)) + { + /* errors have already been logged */ + return false; + } + + /* when everything fails, we return a proper empty string buffer */ + bzero((void *) ipaddr, size); + + /* loop over the forward DNS results for hostname */ + for (ai = lookup; ai; ai = ai->ai_next) + { + char candidateIPAddr[BUFSIZE] = { 0 }; + char hbuf[NI_MAXHOST] = { 0 }; + + if (!ipaddr_sockaddr_to_string(ai, candidateIPAddr, BUFSIZE)) + { + /* errors have already been logged */ + continue; + } + + /* keep the first IP address of the list */ + if (IS_EMPTY_STRING_BUFFER(ipaddr)) + { + strlcpy(ipaddr, candidateIPAddr, size); + } + + log_debug("%s has address %s", hostname, candidateIPAddr); + + /* now reverse lookup (NI_NAMEREQD) the address with getnameinfo() */ + int ret = getnameinfo(ai->ai_addr, ai->ai_addrlen, + hbuf, sizeof(hbuf), NULL, 0, NI_NAMEREQD); + + if (ret != 0) + { + log_debug("Failed to resolve hostname from address \"%s\": %s", + ipaddr, gai_strerror(ret)); + continue; + } + + log_debug("reverse lookup for \"%s\" gives \"%s\" first", + candidateIPAddr, hbuf); + + /* compare reverse-DNS lookup result with our hostname */ + if (strcmp(hbuf, hostname) == 0) + { + *foundHostnameFromAddress = true; + break; + } + } + freeaddrinfo(lookup); + + return true; +} + + +/* + * ipaddr_sockaddr_to_string converts a binary socket address to its string + * representation using inet_ntop(3). + */ +static bool +ipaddr_sockaddr_to_string(struct addrinfo *ai, char *ipaddr, size_t size) +{ + if (ai->ai_family == AF_INET) + { + struct sockaddr_in *ip = (struct sockaddr_in *) ai->ai_addr; + + if (inet_ntop(AF_INET, (void *) &(ip->sin_addr), ipaddr, size) == NULL) + { + log_debug("Failed to determine local ip address: %m"); + return false; + } + } + else if (ai->ai_family == AF_INET6) + { + struct sockaddr_in6 *ip = (struct sockaddr_in6 *) ai->ai_addr; + + if (inet_ntop(AF_INET6, (void *) &(ip->sin6_addr), ipaddr, size) == NULL) + { + log_debug("Failed to determine local ip address: %m"); + return false; + } + } + else + { + /* Highly unexpected */ + log_debug("Non supported ai_family %d", ai->ai_family); + return false; + } + + return true; +} + + +/* + * ipaddr_getsockname gets the IP address "name" from a connected socket. + */ +static bool +ipaddr_getsockname(int sock, char *ipaddr, size_t size) +{ + struct sockaddr_storage address = { 0 }; + socklen_t sockaddrlen = sizeof(address); + + + int err = getsockname(sock, (struct sockaddr *) (&address), &sockaddrlen); + if (err < 0) + { + log_warn("Failed to get IP address from socket: %m"); + return false; + } + + if (address.ss_family == AF_INET) + { + struct sockaddr_in *ip = (struct sockaddr_in *) &address; + + if (inet_ntop(AF_INET, (void *) &(ip->sin_addr), ipaddr, size) == NULL) + { + log_debug("Failed to determine local ip address: %m"); + return false; + } + } + else if (address.ss_family == AF_INET6) + { + struct sockaddr_in6 *ip = (struct sockaddr_in6 *) &address; + + if (inet_ntop(AF_INET6, (void *) &(ip->sin6_addr), ipaddr, size) == NULL) + { + log_debug("Failed to determine local ip address: %m"); + return false; + } + } + else + { + log_debug("Non supported ss_family %d", address.ss_family); + return false; + } + + return true; +} + + +/* + * ipaddrGetLocalHostname uses gethostname(3) to get the current machine + * hostname. We only use the result from gethostname(3) when in turn we can + * resolve the result to an IP address that is present on the local machine. + * + * Failing to match the hostname to a local IP address, we then use the default + * lookup service name and port instead (we would then connect to a google + * provided DNS service to see what is the default network interface/source + * address to connect to a remote endpoint; to avoid any of that process just + * using pg_autoctl with the --hostname option). + */ +bool +ipaddrGetLocalHostname(char *hostname, size_t size) +{ + char localIpAddress[BUFSIZE] = { 0 }; + char hostnameCandidate[_POSIX_HOST_NAME_MAX] = { 0 }; + + if (gethostname(hostnameCandidate, sizeof(hostnameCandidate)) == -1) + { + log_warn("Failed to get local hostname: %m"); + return false; + } + + log_debug("ipaddrGetLocalHostname: \"%s\"", hostnameCandidate); + + /* do a lookup of the host name and see that we get a local address back */ + if (!findHostnameLocalAddress(hostnameCandidate, localIpAddress, BUFSIZE)) + { + log_warn("Failed to get a local IP address for hostname \"%s\"", + hostnameCandidate); + return false; + } + + strlcpy(hostname, hostnameCandidate, size); + + return true; +} + + +/* + * GetAddrInfo calls getaddrinfo and implement a retry policy in case we get a + * transient failure from the system. And for kubernetes compatibility, we also + * retry when the plain EAI_FAIL error code is returned, because DNS entries in + * this environments are dynamic. + */ +static bool +GetAddrInfo(const char *restrict node, + const char *restrict service, + const struct addrinfo *restrict hints, + struct addrinfo **restrict res) +{ + bool success = false; + ConnectionRetryPolicy retryPolicy = { 0 }; + + (void) pgsql_set_interactive_retry_policy(&retryPolicy); + + while (!pgsql_retry_policy_expired(&retryPolicy)) + { + int error = getaddrinfo(node, service, hints, res); + + /* + * Given docker/kubernetes environments, we treat permanent DNS + * failures (EAI_FAIL) as a retryable condition, same as EAI_AGAIN. + */ + if (error != 0 && error != EAI_AGAIN && error != EAI_FAIL) + { + log_warn("Failed to resolve DNS name \"%s\": %s", + node, gai_strerror(error)); + return false; + } + else if (error != 0) + { + log_debug("Failed to resolve DNS name \"%s\": %s", + node, gai_strerror(error)); + } + + success = (error == 0); + + if (success) + { + break; + } + + int sleepTimeMs = + pgsql_compute_connection_retry_sleep_time(&retryPolicy); + + /* we have milliseconds, pg_usleep() wants microseconds */ + (void) pg_usleep(sleepTimeMs * 1000); + } + + return success; +} diff --git a/src/bin/common/ipaddr.h b/src/bin/common/ipaddr.h new file mode 100644 index 000000000..76cf2dbc6 --- /dev/null +++ b/src/bin/common/ipaddr.h @@ -0,0 +1,40 @@ +/* + * src/bin/pg_autoctl/ipaddr.h + * Find local ip used as source ip in ip packets, using getsockname and a udp + * connection. + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the PostgreSQL License. + * + */ + +#ifndef __IPADDRH__ +#define __IPADDRH__ + +#include + + +typedef enum +{ + IPTYPE_V4, IPTYPE_V6, IPTYPE_NONE +} IPType; + + +IPType ip_address_type(const char *hostname); +bool fetchLocalIPAddress(char *localIpAddress, int size, + const char *serviceName, int servicePort, + int logLevel, bool *mayRetry); +bool fetchLocalCIDR(const char *localIpAddress, char *localCIDR, int size); +bool findHostnameLocalAddress(const char *hostname, + char *localIpAddress, int size); +bool findHostnameFromLocalIpAddress(char *localIpAddress, + char *hostname, int size); + +bool resolveHostnameForwardAndReverse(const char *hostname, + char *ipaddr, int size, + bool *foundHostnameFromAddress); + +bool ipaddrGetLocalHostname(char *hostname, size_t size); + + +#endif /* __IPADDRH__ */ diff --git a/src/bin/common/lock_utils.c b/src/bin/common/lock_utils.c new file mode 100644 index 000000000..cd65e0ee7 --- /dev/null +++ b/src/bin/common/lock_utils.c @@ -0,0 +1,382 @@ +/* + * src/bin/pg_autoctl/lock_utils.c + * Implementations of utility functions for inter-process locking + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the PostgreSQL License. + * + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "defaults.h" +#include "file_utils.h" +#include "env_utils.h" +#include "lock_utils.h" +#include "log.h" +#include "pidfile.h" +#include "string_utils.h" + + +/* + * See man semctl(2) + */ +#if defined(__linux__) +union semun +{ + int val; + struct semid_ds *buf; + unsigned short *array; +}; +#endif + +/* + * semaphore_init creates or opens a named semaphore for the current process. + * + * We use the environment variable PG_AUTOCTL_SERVICE to signal when a process + * is a child process of the main pg_autoctl supervisor so that we are able to + * initialize our locking strategy before parsing the command line. After all, + * we might have to log some output during the parsing itself. + */ +bool +semaphore_init(Semaphore *semaphore) +{ + if (env_exists(PG_AUTOCTL_LOG_SEMAPHORE)) + { + return semaphore_open(semaphore); + } + else + { + bool success = semaphore_create(semaphore); + + /* + * Only the main process should unlink the semaphore at exit time. + * + * When we create a semaphore, ensure we put our semId in the expected + * environment variable (PG_AUTOCTL_LOG_SEMAPHORE), and we assign the + * current process' pid as the semaphore owner. + * + * When we open a pre-existing semaphore using PG_AUTOCTL_LOG_SEMAPHORE + * as the semId, the semaphore owner is left to zero. + * + * The atexit(3) function that removes the semaphores only acts when + * the owner is our current pid. That way, in case of an early failure + * in execv(), the semaphore is not dropped from under the main + * program. + * + * A typical way execv() would fail is when calling run_program() on a + * pathname that does not exists. + * + * Per atexit(3) manual page: + * + * When a child process is created via fork(2), it inherits copies of + * its parent's registrations. Upon a successful call to one of the + * exec(3) functions, all registrations are removed. + * + * And that's why it's important that we don't remove the semaphore in + * the atexit() cleanup function when a call to run_command() fails + * early. + */ + if (success) + { + IntString semIdString = intToString(semaphore->semId); + + setenv(PG_AUTOCTL_LOG_SEMAPHORE, semIdString.strValue, 1); + } + + return success; + } +} + + +/* + * semaphore_finish closes or unlinks given semaphore. + */ +bool +semaphore_finish(Semaphore *semaphore) +{ + /* + * At initialization time we either create a new semaphore and register + * getpid() as the owner, or we open a previously existing semaphore from + * its semId as found in our environment variable PG_AUTOCTL_LOG_SEMAPHORE. + * + * At finish time (called from the atexit(3) registry), we remove the + * semaphore only when we are the owner of it. + */ + if (semaphore->owner == getpid()) + { + return semaphore_unlink(semaphore); + } + + return true; +} + + +/* + * semaphore_create creates a new semaphore with the value 1. + */ +bool +semaphore_create(Semaphore *semaphore) +{ + union semun semun; + + semaphore->owner = getpid(); + semaphore->semId = semget(IPC_PRIVATE, 1, 0600); + + if (semaphore->semId < 0) + { + /* the semaphore_log_lock_function has not been set yet */ + log_fatal("Failed to create semaphore: %m\n"); + return false; + } + + /* to see this log line, change the default log level in set_logger() */ + log_trace("Created semaphore %d", semaphore->semId); + + semun.val = 1; + if (semctl(semaphore->semId, 0, SETVAL, semun) < 0) + { + /* the semaphore_log_lock_function has not been set yet */ + log_fatal("Failed to set semaphore %d/%d to value %d : %m\n", + semaphore->semId, 0, semun.val); + return false; + } + + return true; +} + + +/* + * semaphore_open opens our IPC_PRIVATE semaphore. + * + * We don't have a key for it, because we asked the kernel to create a new + * semaphore set with the guarantee that it would not exist already. So we + * re-use the semaphore identifier directly. + * + * We don't even have to call semget(2) here at all, because we share our + * semaphore identifier in the environment directly. + */ +bool +semaphore_open(Semaphore *semaphore) +{ + char semIdString[BUFSIZE] = { 0 }; + + /* ensure the owner is set to zero when we re-open an existing semaphore */ + semaphore->owner = 0; + + if (!get_env_copy(PG_AUTOCTL_LOG_SEMAPHORE, semIdString, BUFSIZE)) + { + /* errors have already been logged */ + return false; + } + + if (!stringToInt(semIdString, &semaphore->semId)) + { + /* errors have already been logged */ + return false; + } + + /* to see this log line, change the default log level in set_logger() */ + log_trace("Using semaphore %d", semaphore->semId); + + /* we have the semaphore identifier, no need to call semget(2), done */ + return true; +} + + +/* + * semaphore_unlink removes an existing named semaphore. + */ +bool +semaphore_unlink(Semaphore *semaphore) +{ + union semun semun; + + semun.val = 0; /* unused, but keep compiler quiet */ + + log_trace("ipcrm -s %d\n", semaphore->semId); + + if (semctl(semaphore->semId, 0, IPC_RMID, semun) < 0) + { + fformat(stderr, "Failed to remove semaphore %d: %m", semaphore->semId); + return false; + } + + return true; +} + + +/* + * semaphore_cleanup is used when we find a stale PID file, to remove a + * possibly left behind semaphore. The user could also use ipcs and ipcrm to + * figure that out, if the stale pidfile does not exist anymore. + */ +bool +semaphore_cleanup(const char *pidfile) +{ + Semaphore semaphore; + + long fileSize = 0L; + char *fileContents = NULL; + char *fileLines[BUFSIZE] = { 0 }; + + if (!file_exists(pidfile)) + { + return false; + } + + if (!read_file(pidfile, &fileContents, &fileSize)) + { + return false; + } + + int lineCount = splitLines(fileContents, fileLines, BUFSIZE); + + if (lineCount < PIDFILE_LINE_SEM_ID) + { + log_debug("Failed to cleanup the semaphore from stale pid file \"%s\": " + "it contains %d lines, semaphore id is expected in line %d", + pidfile, + lineCount, + PIDFILE_LINE_SEM_ID); + free(fileContents); + return false; + } + + if (!stringToInt(fileLines[PIDFILE_LINE_SEM_ID], &(semaphore.semId))) + { + /* errors have already been logged */ + free(fileContents); + return false; + } + + free(fileContents); + + log_trace("Read semaphore id %d from stale pidfile", semaphore.semId); + + return semaphore_unlink(&semaphore); +} + + +/* + * semaphore_lock locks a semaphore (decrement count), blocking if count would + * be < 0 + */ +bool +semaphore_lock(Semaphore *semaphore) +{ + int errStatus; + struct sembuf sops; + + sops.sem_op = -1; /* decrement */ + sops.sem_flg = SEM_UNDO; + sops.sem_num = 0; + + /* + * Note: if errStatus is -1 and errno == EINTR then it means we returned + * from the operation prematurely because we were sent a signal. So we + * try and lock the semaphore again. + * + * We used to check interrupts here, but that required servicing + * interrupts directly from signal handlers. Which is hard to do safely + * and portably. + */ + do { + errStatus = semop(semaphore->semId, &sops, 1); + } while (errStatus < 0 && errno == EINTR); + + if (errStatus < 0) + { + fformat(stderr, + "%d Failed to acquire a lock with semaphore %d: %m\n", + getpid(), + semaphore->semId); + return false; + } + + return true; +} + + +/* + * semaphore_unlock unlocks a semaphore (increment count) + */ +bool +semaphore_unlock(Semaphore *semaphore) +{ + int errStatus; + struct sembuf sops; + + sops.sem_op = 1; /* increment */ + sops.sem_flg = SEM_UNDO; + sops.sem_num = 0; + + /* + * Note: if errStatus is -1 and errno == EINTR then it means we returned + * from the operation prematurely because we were sent a signal. So we + * try and unlock the semaphore again. Not clear this can really happen, + * but might as well cope. + */ + do { + errStatus = semop(semaphore->semId, &sops, 1); + } while (errStatus < 0 && errno == EINTR); + + if (errStatus < 0) + { + fformat(stderr, + "Failed to release a lock with semaphore %d: %m\n", + semaphore->semId); + return false; + } + + return true; +} + + +/* + * semaphore_log_lock_function integrates our semaphore facility with the + * logging tool in use in this project. + */ +void +semaphore_log_lock_function(void *udata, int mode) +{ + Semaphore *semaphore = (Semaphore *) udata; + + /* + * If locking/unlocking fails for some weird reason, we still want to log. + * It's not so bad that we want to completely quit the program. + * That's why we ignore the return values of semaphore_unlock and + * semaphore_lock. + */ + + switch (mode) + { + /* unlock */ + case 0: + { + (void) semaphore_unlock(semaphore); + break; + } + + /* lock */ + case 1: + { + (void) semaphore_lock(semaphore); + break; + } + + default: + { + fformat(stderr, + "BUG: semaphore_log_lock_function called with mode %d", + mode); + exit(EXIT_CODE_INTERNAL_ERROR); + } + } +} diff --git a/src/bin/common/lock_utils.h b/src/bin/common/lock_utils.h new file mode 100644 index 000000000..465c15df7 --- /dev/null +++ b/src/bin/common/lock_utils.h @@ -0,0 +1,40 @@ +/* + * src/bin/pg_autoctl/lock_utils.h + * Utility functions for inter-process locking + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the PostgreSQL License. + * + */ + +#ifndef LOCK_UTILS_H +#define LOCK_UTILS_H + +#include + +#include +#include +#include + +typedef struct Semaphore +{ + int semId; + pid_t owner; +} Semaphore; + + +bool semaphore_init(Semaphore *semaphore); +bool semaphore_finish(Semaphore *semaphore); + +bool semaphore_create(Semaphore *semaphore); +bool semaphore_open(Semaphore *semaphore); +bool semaphore_unlink(Semaphore *semaphore); + +bool semaphore_cleanup(const char *pidfile); + +bool semaphore_lock(Semaphore *semaphore); +bool semaphore_unlock(Semaphore *semaphore); + +void semaphore_log_lock_function(void *udata, int mode); + +#endif /* LOCK_UTILS_H */ diff --git a/src/bin/common/parsing.c b/src/bin/common/parsing.c new file mode 100644 index 000000000..80d75a332 --- /dev/null +++ b/src/bin/common/parsing.c @@ -0,0 +1,1221 @@ +/* + * src/bin/pg_autoctl/parsing.c + * API for parsing the output of some PostgreSQL server commands. + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the PostgreSQL License. + * + */ + +#include +#include +#include +#include +#include +#include + +#include "parson.h" + +#include "log.h" +#include "nodestate_utils.h" +#include "parsing.h" +#include "string_utils.h" + +static bool parse_controldata_field_dbstate(const char *controlDataString, + DBState *state); + +static bool parse_controldata_field_uint32(const char *controlDataString, + const char *fieldName, + uint32_t *dest); + +static bool parse_controldata_field_uint64(const char *controlDataString, + const char *fieldName, + uint64_t *dest); + +static bool parse_controldata_field_lsn(const char *controlDataString, + const char *fieldName, + char lsn[]); + +static bool parse_bool_with_len(const char *value, size_t len, bool *result); + +static int nodeAddressCmpByNodeId(const void *a, const void *b); + +#define RE_MATCH_COUNT 10 + + +/* + * Simple Regexp matching that returns the first matching element. + */ +char * +regexp_first_match(const char *string, const char *regex) +{ + regex_t compiledRegex; + + regmatch_t m[RE_MATCH_COUNT]; + + if (string == NULL) + { + return NULL; + } + + int status = regcomp(&compiledRegex, regex, REG_EXTENDED | REG_NEWLINE); + + if (status != 0) + { + /* + * regerror() returns how many bytes are actually needed to host the + * error message, and truncates the error message when it doesn't fit + * in given size. If the message has been truncated, then we add an + * ellispis to our log entry. + * + * We could also dynamically allocate memory for the error message, but + * the error might be "out of memory" already... + */ + char message[BUFSIZE]; + size_t bytes = regerror(status, &compiledRegex, message, BUFSIZE); + + log_error("Failed to compile regex \"%s\": %s%s", + regex, message, bytes < BUFSIZE ? "..." : ""); + + regfree(&compiledRegex); + + return NULL; + } + + /* + * regexec returns 0 if the regular expression matches; otherwise, it + * returns a nonzero value. + */ + int matchStatus = regexec(&compiledRegex, string, RE_MATCH_COUNT, m, 0); + regfree(&compiledRegex); + + /* We're interested into 1. re matches 2. captured at least one group */ + if (matchStatus != 0 || m[0].rm_so == -1 || m[1].rm_so == -1) + { + return NULL; + } + else + { + regoff_t start = m[1].rm_so; + regoff_t finish = m[1].rm_eo; + int length = finish - start + 1; + char *result = (char *) malloc(length * sizeof(char)); + + if (result == NULL) + { + log_error(ALLOCATION_FAILED_ERROR); + return NULL; + } + + strlcpy(result, string + start, length); + + return result; + } + return NULL; +} + + +/* + * Parse the version number output from pg_ctl --version: + * pg_ctl (PostgreSQL) 10.3 + */ +bool +parse_version_number(const char *version_string, + char *pg_version_string, + size_t size, + int *pg_version) +{ + char *match = regexp_first_match(version_string, "([0-9.]+)"); + + if (match == NULL) + { + log_error("Failed to parse Postgres version number \"%s\"", + version_string); + return false; + } + + /* first, copy the version number in our expected result string buffer */ + strlcpy(pg_version_string, match, size); + + if (!parse_pg_version_string(pg_version_string, pg_version)) + { + /* errors have already been logged */ + free(match); + return false; + } + + free(match); + return true; +} + + +/* + * parse_dotted_version_string parses a major.minor dotted version string such + * as "12.6" into a single number in the same format as the pg_control_version, + * such as 1206. + */ +bool +parse_dotted_version_string(const char *pg_version_string, int *pg_version) +{ + /* now, parse the numbers into an integer, ala pg_control_version */ + bool dotFound = false; + char major[INTSTRING_MAX_DIGITS] = { 0 }; + char minor[INTSTRING_MAX_DIGITS] = { 0 }; + + int majorIdx = 0; + int minorIdx = 0; + + if (pg_version_string == NULL) + { + log_debug("BUG: parse_pg_version_string got NULL"); + return false; + } + + for (int i = 0; pg_version_string[i] != '\0'; i++) + { + if (pg_version_string[i] == '.') + { + if (dotFound) + { + log_error("Failed to parse Postgres version number \"%s\"", + pg_version_string); + return false; + } + + dotFound = true; + continue; + } + + if (dotFound) + { + minor[minorIdx++] = pg_version_string[i]; + } + else + { + major[majorIdx++] = pg_version_string[i]; + } + } + + /* Postgres alpha/beta versions report version "14" instead of "14.0" */ + if (!dotFound) + { + strlcpy(minor, "0", INTSTRING_MAX_DIGITS); + } + + int maj = 0; + int min = 0; + + if (!stringToInt(major, &maj) || + !stringToInt(minor, &min)) + { + log_error("Failed to parse Postgres version number \"%s\"", + pg_version_string); + return false; + } + + /* transform "12.6" into 1206, that is 12 * 100 + 6 */ + *pg_version = (maj * 100) + min; + + return true; +} + + +/* + * parse_pg_version_string parses a Postgres version string such as "12.6" into + * a single number in the same format as the pg_control_version, such as 1206. + */ +bool +parse_pg_version_string(const char *pg_version_string, int *pg_version) +{ + return parse_dotted_version_string(pg_version_string, pg_version); +} + + +/* + * parse_pgaf_version_string parses a pg_auto_failover version string such as + * "1.4" into a single number in the same format as the pg_control_version, + * such as 104. + */ +bool +parse_pgaf_extension_version_string(const char *version_string, int *version) +{ + return parse_dotted_version_string(version_string, version); +} + + +/* + * Parse the first 3 lines of output from pg_controldata: + * + * pg_control version number: 1002 + * Catalog version number: 201707211 + * Database system identifier: 6534312872085436521 + * + */ +bool +parse_controldata(PostgresControlData *pgControlData, + const char *control_data_string) +{ + if (!parse_controldata_field_dbstate(control_data_string, + &(pgControlData->state)) || + + !parse_controldata_field_uint32(control_data_string, + "pg_control version number", + &(pgControlData->pg_control_version)) || + + !parse_controldata_field_uint32(control_data_string, + "Catalog version number", + &(pgControlData->catalog_version_no)) || + + !parse_controldata_field_uint64(control_data_string, + "Database system identifier", + &(pgControlData->system_identifier)) || + + !parse_controldata_field_lsn(control_data_string, + "Latest checkpoint location", + pgControlData->latestCheckpointLSN) || + + !parse_controldata_field_uint32(control_data_string, + "Latest checkpoint's TimeLineID", + &(pgControlData->timeline_id))) + { + log_error("Failed to parse pg_controldata output"); + return false; + } + return true; +} + + +#define streq(x, y) ((x != NULL) && (y != NULL) && (strcmp(x, y) == 0)) + +/* + * parse_controldata_field_dbstate matches pg_controldata output for Database + * cluster state and fills in the value string as an enum value. + */ +static bool +parse_controldata_field_dbstate(const char *controlDataString, DBState *state) +{ + char regex[BUFSIZE] = { 0 }; + + sformat(regex, BUFSIZE, "Database cluster state: *(.*)$"); + + char *match = regexp_first_match(controlDataString, regex); + + if (match == NULL) + { + return false; + } + + if (streq(match, "starting up")) + { + *state = DB_STARTUP; + } + else if (streq(match, "shut down")) + { + *state = DB_SHUTDOWNED; + } + else if (streq(match, "shut down in recovery")) + { + *state = DB_SHUTDOWNED_IN_RECOVERY; + } + else if (streq(match, "shutting down")) + { + *state = DB_SHUTDOWNING; + } + else if (streq(match, "in crash recovery")) + { + *state = DB_IN_CRASH_RECOVERY; + } + else if (streq(match, "in archive recovery")) + { + *state = DB_IN_ARCHIVE_RECOVERY; + } + else if (streq(match, "in production")) + { + *state = DB_IN_PRODUCTION; + } + else + { + log_error("Failed to parse database cluster state \"%s\"", match); + free(match); + return false; + } + + free(match); + return true; +} + + +/* + * parse_controldata_field_uint32 matches pg_controldata output for a field + * name and gets its value as an uint64_t. It returns false when something went + * wrong, and true when the value can be used. + */ +static bool +parse_controldata_field_uint32(const char *controlDataString, + const char *fieldName, + uint32_t *dest) +{ + char regex[BUFSIZE]; + + sformat(regex, BUFSIZE, "^%s: *([0-9]+)$", fieldName); + char *match = regexp_first_match(controlDataString, regex); + + if (match == NULL) + { + return false; + } + + if (!stringToUInt32(match, dest)) + { + log_error("Failed to parse number \"%s\": %m", match); + free(match); + return false; + } + + free(match); + return true; +} + + +/* + * parse_controldata_field_uint64 matches pg_controldata output for a field + * name and gets its value as an uint64_t. It returns false when something went + * wrong, and true when the value can be used. + */ +static bool +parse_controldata_field_uint64(const char *controlDataString, + const char *fieldName, + uint64_t *dest) +{ + char regex[BUFSIZE]; + + sformat(regex, BUFSIZE, "^%s: *([0-9]+)$", fieldName); + char *match = regexp_first_match(controlDataString, regex); + + if (match == NULL) + { + return false; + } + + if (!stringToUInt64(match, dest)) + { + log_error("Failed to parse number \"%s\": %m", match); + free(match); + return false; + } + + free(match); + return true; +} + + +/* + * parse_controldata_field_lsn matches pg_controldata output for a field name + * and gets its value as a string, in an area that must be pre-allocated with + * at least PG_LSN_MAXLENGTH bytes. + */ +static bool +parse_controldata_field_lsn(const char *controlDataString, + const char *fieldName, + char lsn[]) +{ + char regex[BUFSIZE]; + + sformat(regex, BUFSIZE, "^%s: *([0-9A-F]+/[0-9A-F]+)$", fieldName); + char *match = regexp_first_match(controlDataString, regex); + + if (match == NULL) + { + return false; + } + + strlcpy(lsn, match, PG_LSN_MAXLENGTH); + + free(match); + return true; +} + + +/* + * parse_notification_message parses pgautofailover state change notifications, + * which are sent in the JSON format. + */ +bool +parse_state_notification_message(CurrentNodeState *nodeState, + const char *message) +{ + JSON_Value *json = json_parse_string(message); + JSON_Object *jsobj = json_value_get_object(json); + + log_trace("parse_state_notification_message: %s", message); + + if (json_type(json) != JSONObject) + { + log_error("Failed to parse JSON notification message: \"%s\"", message); + json_value_free(json); + return false; + } + + char *str = (char *) json_object_get_string(jsobj, "type"); + + if (strcmp(str, "state") != 0) + { + log_error("Failed to parse JSOBJ notification state message: " + "jsobj object type is not \"state\" as expected"); + json_value_free(json); + return false; + } + + str = (char *) json_object_get_string(jsobj, "formation"); + + if (str == NULL) + { + log_error("Failed to parse formation in JSON " + "notification message \"%s\"", message); + json_value_free(json); + return false; + } + strlcpy(nodeState->formation, str, sizeof(nodeState->formation)); + + double number = json_object_get_number(jsobj, "groupId"); + nodeState->groupId = (int) number; + + number = json_object_get_number(jsobj, "nodeId"); + nodeState->node.nodeId = (int) number; + + str = (char *) json_object_get_string(jsobj, "name"); + + if (str == NULL) + { + log_error("Failed to parse node name in JSON " + "notification message \"%s\"", message); + json_value_free(json); + return false; + } + strlcpy(nodeState->node.name, str, sizeof(nodeState->node.name)); + + str = (char *) json_object_get_string(jsobj, "host"); + + if (str == NULL) + { + log_error("Failed to parse node host in JSON " + "notification message \"%s\"", message); + json_value_free(json); + return false; + } + strlcpy(nodeState->node.host, str, sizeof(nodeState->node.host)); + + number = json_object_get_number(jsobj, "port"); + nodeState->node.port = (int) number; + + str = (char *) json_object_get_string(jsobj, "reportedState"); + + if (str == NULL) + { + log_error("Failed to parse reportedState in JSON " + "notification message \"%s\"", message); + json_value_free(json); + return false; + } + nodeState->reportedState = NodeStateFromString(str); + + str = (char *) json_object_get_string(jsobj, "goalState"); + + if (str == NULL) + { + log_error("Failed to parse goalState in JSON " + "notification message \"%s\"", message); + json_value_free(json); + return false; + } + nodeState->goalState = NodeStateFromString(str); + + str = (char *) json_object_get_string(jsobj, "health"); + + if (streq(str, "unknown")) + { + nodeState->health = -1; + } + else if (streq(str, "bad")) + { + nodeState->health = 0; + } + else if (streq(str, "good")) + { + nodeState->health = 1; + } + else + { + log_error("Failed to parse health in JSON " + "notification message \"%s\"", message); + json_value_free(json); + return false; + } + + json_value_free(json); + return true; +} + + +/* + * Try to interpret value as boolean value. Valid values are: true, + * false, yes, no, on, off, 1, 0; as well as unique prefixes thereof. + * If the string parses okay, return true, else false. + * If okay and result is not NULL, return the value in *result. + * + * Copied from PostgreSQL sources + * file : src/backend/utils/adt/bool.c + */ +static bool +parse_bool_with_len(const char *value, size_t len, bool *result) +{ + switch (*value) + { + case 't': + case 'T': + { + if (pg_strncasecmp(value, "true", len) == 0) + { + if (result) + { + *result = true; + } + return true; + } + break; + } + + case 'f': + case 'F': + { + if (pg_strncasecmp(value, "false", len) == 0) + { + if (result) + { + *result = false; + } + return true; + } + break; + } + + case 'y': + case 'Y': + { + if (pg_strncasecmp(value, "yes", len) == 0) + { + if (result) + { + *result = true; + } + return true; + } + break; + } + + case 'n': + case 'N': + { + if (pg_strncasecmp(value, "no", len) == 0) + { + if (result) + { + *result = false; + } + return true; + } + break; + } + + case 'o': + case 'O': + { + /* 'o' is not unique enough */ + if (pg_strncasecmp(value, "on", (len > 2 ? len : 2)) == 0) + { + if (result) + { + *result = true; + } + return true; + } + else if (pg_strncasecmp(value, "off", (len > 2 ? len : 2)) == 0) + { + if (result) + { + *result = false; + } + return true; + } + break; + } + + case '1': + { + if (len == 1) + { + if (result) + { + *result = true; + } + return true; + } + break; + } + + case '0': + { + if (len == 1) + { + if (result) + { + *result = false; + } + return true; + } + break; + } + + default: + { + break; + } + } + + if (result) + { + *result = false; /* suppress compiler warning */ + } + return false; +} + + +/* + * parse_bool parses boolean text value (true/false/on/off/yes/no/1/0) and + * puts the boolean value back in the result field if it is not NULL. + * The function returns true on successful parse, returns false if any parse + * error is encountered. + */ +bool +parse_bool(const char *value, bool *result) +{ + return parse_bool_with_len(value, strlen(value), result); +} + + +/* + * parse_pguri_info_key_vals decomposes elements of a Postgres connection + * string (URI) into separate arrays of keywords and values as expected by + * PQconnectdbParams. + */ +bool +parse_pguri_info_key_vals(const char *pguri, + KeyVal *overrides, + URIParams *uriParameters, + bool checkForCompleteURI) +{ + char *errmsg; + PQconninfoOption *conninfo, *option; + + bool foundHost = false; + bool foundUser = false; + bool foundPort = false; + bool foundDBName = false; + + int paramIndex = 0; + + conninfo = PQconninfoParse(pguri, &errmsg); + if (conninfo == NULL) + { + log_error("Failed to parse pguri \"%s\": %s", pguri, errmsg); + PQfreemem(errmsg); + return false; + } + + for (option = conninfo; option->keyword != NULL; option++) + { + char *value = NULL; + int ovIndex = 0; + + /* + * If the keyword is in our overrides array, use the value from the + * override values. Yeah that's O(n*m) but here m is expected to be + * something very small, like 3 (typically: sslmode, sslrootcert, + * sslcrl). + */ + for (ovIndex = 0; ovIndex < overrides->count; ovIndex++) + { + if (strcmp(overrides->keywords[ovIndex], option->keyword) == 0) + { + value = overrides->values[ovIndex]; + } + } + + /* not found in the override, keep the original, or skip */ + if (value == NULL) + { + if (option->val == NULL || strcmp(option->val, "") == 0) + { + continue; + } + else + { + value = option->val; + } + } + + if (strcmp(option->keyword, "host") == 0 || + strcmp(option->keyword, "hostaddr") == 0) + { + foundHost = true; + strlcpy(uriParameters->hostname, option->val, MAXCONNINFO); + } + else if (strcmp(option->keyword, "port") == 0) + { + foundPort = true; + strlcpy(uriParameters->port, option->val, MAXCONNINFO); + } + else if (strcmp(option->keyword, "user") == 0) + { + foundUser = true; + strlcpy(uriParameters->username, option->val, MAXCONNINFO); + } + else if (strcmp(option->keyword, "dbname") == 0) + { + foundDBName = true; + strlcpy(uriParameters->dbname, option->val, MAXCONNINFO); + } + else if (!IS_EMPTY_STRING_BUFFER(value)) + { + /* make a copy in our key/val arrays */ + strlcpy(uriParameters->parameters.keywords[paramIndex], + option->keyword, + MAXCONNINFO); + + strlcpy(uriParameters->parameters.values[paramIndex], + value, + MAXCONNINFO); + + ++uriParameters->parameters.count; + ++paramIndex; + } + } + + PQconninfoFree(conninfo); + + /* + * Display an error message per missing field, and only then return false + * if we're missing any one of those. + */ + if (checkForCompleteURI) + { + if (!foundHost) + { + log_error("Failed to find hostname in the pguri \"%s\"", pguri); + } + + if (!foundPort) + { + log_error("Failed to find port in the pguri \"%s\"", pguri); + } + + if (!foundUser) + { + log_error("Failed to find username in the pguri \"%s\"", pguri); + } + + if (!foundDBName) + { + log_error("Failed to find dbname in the pguri \"%s\"", pguri); + } + + return foundHost && foundPort && foundUser && foundDBName; + } + else + { + return true; + } +} + + +/* + * buildPostgresURIfromPieces builds a Postgres connection string from keywords + * and values, in a user friendly way. The pguri parameter should point to a + * memory area that has been allocated by the caller and has at least + * MAXCONNINFO bytes. + */ +bool +buildPostgresURIfromPieces(URIParams *uriParams, char *pguri) +{ + int index = 0; + + sformat(pguri, MAXCONNINFO, + "postgres://%s@%s:%s/%s?", + uriParams->username, + uriParams->hostname, + uriParams->port, + uriParams->dbname); + + for (index = 0; index < uriParams->parameters.count; index++) + { + if (index == 0) + { + sformat(pguri, MAXCONNINFO, + "%s%s=%s", + pguri, + uriParams->parameters.keywords[index], + uriParams->parameters.values[index]); + } + else + { + sformat(pguri, MAXCONNINFO, + "%s&%s=%s", + pguri, + uriParams->parameters.keywords[index], + uriParams->parameters.values[index]); + } + } + + return true; +} + + +/* + * parse_pguri_ssl_settings parses SSL settings from a Postgres connection + * string. Given the following connection string + * + * "postgres://autoctl_node@localhost:5500/pg_auto_failover?sslmode=prefer" + * + * we then have an ssl->active = 1, ssl->sslMode = SSL_MODE_PREFER, etc. + */ +bool +parse_pguri_ssl_settings(const char *pguri, SSLOptions *ssl) +{ + URIParams params = { 0 }; + KeyVal overrides = { 0 }; + + bool checkForCompleteURI = true; + + /* initialize SSL Params values */ + if (!parse_pguri_info_key_vals(pguri, + &overrides, + ¶ms, + checkForCompleteURI)) + { + /* errors have already been logged */ + return false; + } + + for (int index = 0; index < params.parameters.count; index++) + { + char *key = params.parameters.keywords[index]; + char *val = params.parameters.values[index]; + + if (streq(key, "sslmode")) + { + ssl->sslMode = pgsetup_parse_sslmode(val); + strlcpy(ssl->sslModeStr, val, sizeof(ssl->sslModeStr)); + + if (ssl->sslMode > SSL_MODE_DISABLE) + { + ssl->active = true; + } + } + else if (streq(key, "sslrootcert")) + { + strlcpy(ssl->caFile, val, sizeof(ssl->caFile)); + } + else if (streq(key, "sslcrl")) + { + strlcpy(ssl->crlFile, val, sizeof(ssl->crlFile)); + } + else if (streq(key, "sslcert")) + { + strlcpy(ssl->serverCert, val, sizeof(ssl->serverCert)); + } + else if (streq(key, "sslkey")) + { + strlcpy(ssl->serverKey, val, sizeof(ssl->serverKey)); + } + } + + /* cook-in defaults when the parsed URL contains no SSL settings */ + if (ssl->sslMode == SSL_MODE_UNKNOWN) + { + ssl->active = true; + ssl->sslMode = SSL_MODE_PREFER; + strlcpy(ssl->sslModeStr, + pgsetup_sslmode_to_string(ssl->sslMode), + sizeof(ssl->sslModeStr)); + } + + return true; +} + + +/* + * nodeAddressCmpByNodeId sorts two given nodeAddress by comparing their + * nodeId. We use this function to be able to pg_qsort() an array of nodes, + * such as when parsing from a JSON file. + */ +static int +nodeAddressCmpByNodeId(const void *a, const void *b) +{ + NodeAddress *nodeA = (NodeAddress *) a; + NodeAddress *nodeB = (NodeAddress *) b; + + return nodeA->nodeId - nodeB->nodeId; +} + + +/* + * parseLSN is based on the Postgres code for pg_lsn_in_internal found at + * src/backend/utils/adt/pg_lsn.c in the Postgres source repository. In the + * pg_auto_failover context we don't need to typedef uint64 XLogRecPtr; so we + * just use uint64_t internally. + */ +#define MAXPG_LSNCOMPONENT 8 + +bool +parseLSN(const char *str, uint64_t *lsn) +{ + int len1, + len2; + uint32 id, + off; + + /* Sanity check input format. */ + len1 = strspn(str, "0123456789abcdefABCDEF"); + if (len1 < 1 || len1 > MAXPG_LSNCOMPONENT || str[len1] != '/') + { + return false; + } + + len2 = strspn(str + len1 + 1, "0123456789abcdefABCDEF"); + if (len2 < 1 || len2 > MAXPG_LSNCOMPONENT || str[len1 + 1 + len2] != '\0') + { + return false; + } + + /* Decode result. */ + id = (uint32) strtoul(str, NULL, 16); + off = (uint32) strtoul(str + len1 + 1, NULL, 16); + *lsn = ((uint64) id << 32) | off; + + return true; +} + + +/* + * parseNodesArrayFromFile parses a Nodes Array from a JSON file, that contains + * an array of JSON object with the following properties: node_id, node_lsn, + * node_host, node_name, node_port, and potentially node_is_primary. + */ +bool +parseNodesArray(const char *nodesJSON, + NodeAddressArray *nodesArray, + int64_t nodeId) +{ + JSON_Value *template = + json_parse_string("[{" + "\"node_id\": 0," + "\"node_lsn\": \"\"," + "\"node_name\": \"\"," + "\"node_host\": \"\"," + "\"node_port\": 0," + "\"node_is_primary\": false" + "}]"); + int nodesArrayIndex = 0; + int primaryCount = 0; + + JSON_Value *json = json_parse_string(nodesJSON); + + /* validate the JSON input as an array of object with required fields */ + if (json_validate(template, json) == JSONFailure) + { + log_error("Failed to parse nodes array which is expected " + "to contain a JSON Array of Objects with properties " + "[{node_id:number, node_name:string, " + "node_host:string, node_port:number, node_lsn:string, " + "node_is_primary:boolean}, ...]"); + json_value_free(template); + json_value_free(json); + return false; + } + + JSON_Array *jsArray = json_value_get_array(json); + int len = json_array_get_count(jsArray); + + if (NODE_ARRAY_MAX_COUNT < len) + { + log_error("Failed to parse nodes array which contains " + "%d nodes: pg_autoctl supports up to %d nodes", + len, + NODE_ARRAY_MAX_COUNT); + json_value_free(template); + json_value_free(json); + return false; + } + + nodesArray->count = len; + + for (int i = 0; i < len; i++) + { + NodeAddress *node = &(nodesArray->nodes[nodesArrayIndex]); + JSON_Object *jsObj = json_array_get_object(jsArray, i); + + int jsNodeId = (int) json_object_get_number(jsObj, "node_id"); + uint64_t lsn = 0; + + /* we install the keeper.otherNodes array, so skip ourselves */ + if (jsNodeId == nodeId) + { + --(nodesArray->count); + continue; + } + + node->nodeId = jsNodeId; + + strlcpy(node->name, + json_object_get_string(jsObj, "node_name"), + sizeof(node->name)); + + strlcpy(node->host, + json_object_get_string(jsObj, "node_host"), + sizeof(node->host)); + + node->port = (int) json_object_get_number(jsObj, "node_port"); + + strlcpy(node->lsn, + json_object_get_string(jsObj, "node_lsn"), + sizeof(node->lsn)); + + if (!parseLSN(node->lsn, &lsn)) + { + log_error("Failed to parse nodes array LSN value \"%s\"", node->lsn); + json_value_free(template); + json_value_free(json); + return false; + } + + node->isPrimary = json_object_get_boolean(jsObj, "node_is_primary"); + + if (node->isPrimary) + { + ++primaryCount; + + if (primaryCount > 1) + { + log_error("Failed to parse nodes array: more than one node " + "is listed with \"node_is_primary\" true."); + json_value_free(template); + json_value_free(json); + return false; + } + } + + ++nodesArrayIndex; + } + + json_value_free(template); + json_value_free(json); + + /* now ensure the array is sorted by nodeId */ + (void) pg_qsort(nodesArray->nodes, + nodesArray->count, + sizeof(NodeAddress), + nodeAddressCmpByNodeId); + + /* check that every node id is unique in our array */ + for (int i = 0; i < (nodesArray->count - 1); i++) + { + int currentNodeId = nodesArray->nodes[i].nodeId; + int nextNodeId = nodesArray->nodes[i + 1].nodeId; + + if (currentNodeId == nextNodeId) + { + log_error("Failed to parse nodes array: more than one node " + "is listed with the same nodeId %d", + currentNodeId); + return false; + } + } + + return true; +} + + +/* + * uri_contains_password takes a Postgres connection string and checks to see + * if it contains a parameter called password. Returns true if a password + * keyword is present in the connection string. + */ +static bool +uri_contains_password(const char *pguri) +{ + char *errmsg; + PQconninfoOption *conninfo, *option; + + conninfo = PQconninfoParse(pguri, &errmsg); + if (conninfo == NULL) + { + log_error("Failed to parse pguri: %s", errmsg); + + PQfreemem(errmsg); + return false; + } + + /* + * Look for a populated password connection parameter + */ + for (option = conninfo; option->keyword != NULL; option++) + { + if (strcmp(option->keyword, "password") == 0 && + option->val != NULL && + !IS_EMPTY_STRING_BUFFER(option->val)) + { + PQconninfoFree(conninfo); + return true; + } + } + + PQconninfoFree(conninfo); + return false; +} + + +/* + * parse_and_scrub_connection_string takes a Postgres connection string and + * populates scrubbedPguri with the password replaced with **** for logging. + * The scrubbedPguri parameter should point to a memory area that has been + * allocated by the caller and has at least MAXCONNINFO bytes. + */ +bool +parse_and_scrub_connection_string(const char *pguri, char *scrubbedPguri) +{ + URIParams uriParams = { 0 }; + KeyVal overrides = { 0 }; + + if (uri_contains_password(pguri)) + { + overrides = (KeyVal) { + .count = 1, + .keywords = { "password" }, + .values = { "****" } + }; + } + + bool checkForCompleteURI = false; + + if (!parse_pguri_info_key_vals(pguri, + &overrides, + &uriParams, + checkForCompleteURI)) + { + return false; + } + + buildPostgresURIfromPieces(&uriParams, scrubbedPguri); + + return true; +} diff --git a/src/bin/common/parsing.h b/src/bin/common/parsing.h new file mode 100644 index 000000000..6b004549d --- /dev/null +++ b/src/bin/common/parsing.h @@ -0,0 +1,100 @@ +/* + * src/bin/pg_autoctl/parsing.c + * API for parsing the output of some PostgreSQL server commands. + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the PostgreSQL License. + * + */ + +#ifndef PARSING_H +#define PARSING_H + +#include + +#include "monitor.h" +#include "nodestate_utils.h" +#include "pgctl.h" + +char * regexp_first_match(const char *string, const char *re); + +bool parse_version_number(const char *version_string, + char *pg_version_string, + size_t size, + int *pg_version); + +bool parse_dotted_version_string(const char *pg_version_string, + int *pg_version); +bool parse_pg_version_string(const char *pg_version_string, + int *pg_version); +bool parse_pgaf_extension_version_string(const char *version_string, + int *version); + +bool parse_controldata(PostgresControlData *pgControlData, + const char *control_data_string); + +bool parse_state_notification_message(CurrentNodeState *nodeState, + const char *message); + +bool parse_bool(const char *value, bool *result); + +#define boolToString(value) (value) ? "true" : "false" + + +/* + * To parse Postgres URI we need to store keywords and values in separate + * arrays of strings, because that's the libpq way of doing things. + * + * keywords and values are arrays of string and the arrays must be large enough + * to fit all the connection parameters (of which we count 36 at the moment on + * the Postgres documentation). + * + * See https://www.postgresql.org/docs/current/libpq-connect.html + * + * So here we use 64 entries each of MAXCONNINFO, to ensure we have enough room + * to store all the parts of a typicallay MAXCONNINFO bounded full URI. That + * amounts to 64kB of memory, so that's not even a luxury. + */ +typedef struct KeyVal +{ + int count; + char keywords[64][MAXCONNINFO]; + char values[64][MAXCONNINFO]; +} KeyVal; + + +/* + * In our own internal processing of Postgres URIs, we want to have some of the + * URL parts readily accessible by name rather than mixed in the KeyVal + * structure. + * + * That's mostly becase we want to produce an URI with the following form: + * + * postgres://user@host:port/dbname?opt=val + */ +typedef struct URIParams +{ + char username[MAXCONNINFO]; + char hostname[MAXCONNINFO]; + char port[MAXCONNINFO]; + char dbname[MAXCONNINFO]; + KeyVal parameters; +} URIParams; + +bool parse_pguri_info_key_vals(const char *pguri, + KeyVal *overrides, + URIParams *uriParameters, + bool checkForCompleteURI); + +bool buildPostgresURIfromPieces(URIParams *uriParams, char *pguri); + +bool parse_pguri_ssl_settings(const char *pguri, SSLOptions *ssl); + +bool parse_and_scrub_connection_string(const char *pguri, char *scrubbedPguri); + +bool parseLSN(const char *str, uint64_t *lsn); +bool parseNodesArray(const char *nodesJSON, + NodeAddressArray *nodesArray, + int64_t nodeId); + +#endif /* PARSING_H */ diff --git a/src/bin/common/pgctl.c b/src/bin/common/pgctl.c new file mode 100644 index 000000000..8ad756ae3 --- /dev/null +++ b/src/bin/common/pgctl.c @@ -0,0 +1,2854 @@ +/* + * src/bin/pg_autoctl/pgctl.c + * API for controling PostgreSQL, using its binary tooling (pg_ctl, + * pg_controldata, pg_basebackup and such). + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the PostgreSQL License. + * + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "postgres_fe.h" +#include "pqexpbuffer.h" + +#include "defaults.h" +#include "env_utils.h" +#include "file_utils.h" +#include "log.h" +#include "parsing.h" +#include "pgctl.h" +#include "pgsql.h" +#include "pgsetup.h" +#include "pgtuning.h" +#include "signals.h" +#include "string_utils.h" + +#define RUN_PROGRAM_IMPLEMENTATION +#include "runprogram.h" + +#define AUTOCTL_CONF_INCLUDE_COMMENT \ + " # Auto-generated by pg_auto_failover, do not remove\n" + +#define AUTOCTL_CONF_INCLUDE_LINE "include '" AUTOCTL_DEFAULTS_CONF_FILENAME "'" +#define AUTOCTL_SB_CONF_INCLUDE_LINE "include '" AUTOCTL_STANDBY_CONF_FILENAME "'" + +static bool pg_include_config(const char *configFilePath, + const char *configIncludeLine, + const char *configIncludeComment); +static bool ensure_default_settings_file_exists(const char *configFilePath, + GUC *settings, + PostgresSetup *pgSetup, + const char *hostname, + bool includeTuning); +static bool prepare_guc_settings_from_pgsetup(const char *configFilePath, + PQExpBuffer config, + GUC *settings, + PostgresSetup *pgSetup, + const char *hostname, + bool includeTuning); +static void log_program_output(Program prog, int outLogLevel, int errorLogLevel); + + +static bool prepare_recovery_settings(const char *pgdata, + ReplicationSource *replicationSource, + char *primaryConnInfo, + char *primarySlotName, + char *targetLSN, + char *targetAction, + char *targetTimeline); + +static bool escape_recovery_conf_string(char *destination, + int destinationSize, + const char *recoveryConfString); +static bool prepare_primary_conninfo(char *primaryConnInfo, + int primaryConnInfoSize, + const char *primaryHost, int primaryPort, + const char *replicationUsername, + const char *dbname, + const char *replicationPassword, + const char *applicationName, + SSLOptions sslOptions, + bool escape); +static bool prepare_conninfo_sslmode(PQExpBuffer buffer, SSLOptions sslOptions); + +static bool pg_write_recovery_conf(const char *pgdata, + ReplicationSource *replicationSource); +static bool pg_write_standby_signal(const char *pgdata, + ReplicationSource *replicationSource); +static bool ensure_empty_tablespace_dirs(const char *pgdata); + +/* + * Get pg_ctl --version output in pgSetup->pg_version. + */ +bool +pg_ctl_version(PostgresSetup *pgSetup) +{ + Program prog = run_program(pgSetup->pg_ctl, "--version", NULL); + char pg_version_string[PG_VERSION_STRING_MAX] = { 0 }; + int pg_version = 0; + + if (prog.returnCode != 0) + { + errno = prog.error; + log_error("Failed to run \"pg_ctl --version\" using program \"%s\": %m", + pgSetup->pg_ctl); + free_program(&prog); + return false; + } + + if (!parse_version_number(prog.stdOut, + pg_version_string, + PG_VERSION_STRING_MAX, + &pg_version)) + { + /* errors have already been logged */ + free_program(&prog); + return false; + } + free_program(&prog); + + strlcpy(pgSetup->pg_version, pg_version_string, PG_VERSION_STRING_MAX); + + return true; +} + + +/* + * set_pg_ctl_from_PG_CONFIG sets given pgSetup->pg_ctl to the pg_ctl binary + * installed in the bindir of the target Postgres installation: + * + * $(${PG_CONFIG} --bindir)/pg_ctl + */ +bool +set_pg_ctl_from_config_bindir(PostgresSetup *pgSetup, const char *pg_config) +{ + char pg_ctl[MAXPGPATH] = { 0 }; + + if (!file_exists(pg_config)) + { + log_debug("set_pg_ctl_from_config_bindir: file not found: \"%s\"", + pg_config); + return false; + } + + Program prog = run_program(pg_config, "--bindir", NULL); + + char *lines[1]; + + if (prog.returnCode != 0) + { + errno = prog.error; + log_error("Failed to run \"pg_config --bindir\" using program \"%s\": %m", + pg_config); + free_program(&prog); + return false; + } + + if (splitLines(prog.stdOut, lines, 1) != 1) + { + log_error("Unable to parse output from pg_config --bindir"); + free_program(&prog); + return false; + } + + char *bindir = lines[0]; + join_path_components(pg_ctl, bindir, "pg_ctl"); + + /* we're now done with the Program and its output */ + free_program(&prog); + + if (!file_exists(pg_ctl)) + { + log_error("Failed to find pg_ctl at \"%s\" from PG_CONFIG at \"%s\"", + pgSetup->pg_ctl, + pg_config); + return false; + } + + strlcpy(pgSetup->pg_ctl, pg_ctl, sizeof(pgSetup->pg_ctl)); + + return true; +} + + +/* + * Read some of the information from pg_controldata output. + */ +bool +pg_controldata(PostgresSetup *pgSetup, bool missing_ok) +{ + char globalControlPath[MAXPGPATH] = { 0 }; + char pg_controldata_path[MAXPGPATH] = { 0 }; + + if (pgSetup->pgdata[0] == '\0' || pgSetup->pg_ctl[0] == '\0') + { + log_error("BUG: pg_controldata: missing pgSetup pgdata or pg_ctl"); + return false; + } + + /* globalControlFilePath = $PGDATA/global/pg_control */ + join_path_components(globalControlPath, pgSetup->pgdata, "global/pg_control"); + + /* + * Refrain from doing too many pg_controldata checks, only proceed when the + * PGDATA/global/pg_control file exists on-disk: that's the first check + * that pg_controldata does anyway. + */ + if (!file_exists(globalControlPath)) + { + return false; + } + + /* now find the pg_controldata binary */ + path_in_same_directory(pgSetup->pg_ctl, "pg_controldata", pg_controldata_path); + log_debug("%s %s", pg_controldata_path, pgSetup->pgdata); + + /* We parse the output of pg_controldata, make sure it's as expected */ + setenv("LANG", "C", 1); + Program prog = run_program(pg_controldata_path, pgSetup->pgdata, NULL); + + if (prog.returnCode == 0) + { + if (prog.stdOut == NULL) + { + /* happens sometimes, and I don't know why */ + log_warn("Got empty output from `%s %s`, trying again in 1s", + pg_controldata_path, pgSetup->pgdata); + sleep(1); + + return pg_controldata(pgSetup, missing_ok); + } + + if (!parse_controldata(&pgSetup->control, prog.stdOut)) + { + log_error("%s %s", pg_controldata_path, pgSetup->pgdata); + log_warn("Failed to parse pg_controldata output:\n%s", prog.stdOut); + free_program(&prog); + return false; + } + + free_program(&prog); + return true; + } + else + { + int errorLogLevel = missing_ok ? LOG_DEBUG : LOG_ERROR; + + (void) log_program_output(prog, LOG_INFO, errorLogLevel); + + log_level(errorLogLevel, + "Failed to run \"%s\" on \"%s\", see above for details", + pg_controldata_path, pgSetup->pgdata); + + free_program(&prog); + + return missing_ok; + } +} + + +/* + * set_pg_ctl_from_PG_CONFIG sets the path to pg_ctl following the exported + * environment variable PG_CONFIG, when it is found in the environment. + * + * Postgres developer environments often define PG_CONFIG in the environment to + * build extensions for a specific version of Postgres. Let's use the hint here + * too. + */ +bool +set_pg_ctl_from_PG_CONFIG(PostgresSetup *pgSetup) +{ + char PG_CONFIG[MAXPGPATH] = { 0 }; + + if (!env_exists("PG_CONFIG")) + { + /* then we don't use PG_CONFIG to find pg_ctl */ + return false; + } + + if (!get_env_copy("PG_CONFIG", PG_CONFIG, sizeof(PG_CONFIG))) + { + /* errors have already been logged */ + return false; + } + + if (!file_exists(PG_CONFIG)) + { + log_error("Failed to find a file for PG_CONFIG environment value \"%s\"", + PG_CONFIG); + return false; + } + + if (!set_pg_ctl_from_config_bindir(pgSetup, PG_CONFIG)) + { + /* errors have already been logged */ + return false; + } + + if (!pg_ctl_version(pgSetup)) + { + log_fatal("Failed to get version info from %s --version", + pgSetup->pg_ctl); + return false; + } + + log_debug("Found pg_ctl for PostgreSQL %s at %s following PG_CONFIG", + pgSetup->pg_version, pgSetup->pg_ctl); + + return true; +} + + +/* + * set_pg_ctl_from_pg_config sets the path to pg_ctl by using pg_config + * --bindir when there is a single pg_config found in the PATH. + * + * When using debian/ubuntu packaging then pg_config is installed as part as + * the postgresql-common package in /usr/bin, whereas pg_ctl is installed in a + * major version dependent location such as /usr/lib/postgresql/12/bin, and + * those locations are not included in the PATH. + * + * So when we can't find pg_ctl anywhere in the PATH, we look for pg_config + * instead, and then use pg_config --bindir to discover the pg_ctl we can use. + */ +bool +set_pg_ctl_from_pg_config(PostgresSetup *pgSetup) +{ + SearchPath all_pg_configs = { 0 }; + SearchPath pg_configs = { 0 }; + + if (!search_path("pg_config", &all_pg_configs)) + { + return false; + } + + if (!search_path_deduplicate_symlinks(&all_pg_configs, &pg_configs)) + { + log_error("Failed to resolve symlinks found in PATH entries, " + "see above for details"); + return false; + } + + switch (pg_configs.found) + { + case 0: + { + log_warn("Failed to find either pg_ctl or pg_config in PATH"); + return false; + } + + case 1: + { + if (!set_pg_ctl_from_config_bindir(pgSetup, pg_configs.matches[0])) + { + /* errors have already been logged */ + return false; + } + + if (!pg_ctl_version(pgSetup)) + { + log_fatal("Failed to get version info from %s --version", + pgSetup->pg_ctl); + return false; + } + + log_debug("Found pg_ctl for PostgreSQL %s at %s from pg_config " + "found in PATH at \"%s\"", + pgSetup->pg_version, + pgSetup->pg_ctl, + pg_configs.matches[0]); + + return true; + } + + default: + { + log_info("Found more than one pg_config entry in current PATH:"); + + for (int i = 0; i < pg_configs.found; i++) + { + PostgresSetup currentPgSetup = { 0 }; + + strlcpy(currentPgSetup.pg_ctl, + pg_configs.matches[i], + sizeof(currentPgSetup.pg_ctl)); + + if (!pg_ctl_version(¤tPgSetup)) + { + /* + * Because of this it's possible that there's now only a + * single working version of pg_ctl found in PATH. If + * that's the case we will still not use that by default, + * since the users intention is unclear. They might have + * wanted to use the version of pg_ctl that we could not + * parse the version string for. So we warn and continue, + * the user should make their intention clear by using the + * --pg_ctl option (or changing PATH). + */ + log_warn("Failed to get version info from %s --version", + currentPgSetup.pg_ctl); + continue; + } + + log_info("Found \"%s\" for pg version %s", + currentPgSetup.pg_ctl, + currentPgSetup.pg_version); + } + + log_info("HINT: export PG_CONFIG to a specific pg_config entry"); + + return false; + } + } + + return false; +} + + +/* + * Find "pg_ctl" programs in the PATH. If a single one exists, set its absolute + * location in pg_ctl, and the PostgreSQL version number in pg_version. + * + * Returns how many "pg_ctl" programs have been found in the PATH. + */ +bool +config_find_pg_ctl(PostgresSetup *pgSetup) +{ + SearchPath all_pg_ctls = { 0 }; + SearchPath pg_ctls = { 0 }; + + pgSetup->pg_ctl[0] = '\0'; + pgSetup->pg_version[0] = '\0'; + + /* + * Postgres developer environments often define PG_CONFIG in the + * environment to build extensions for a specific version of Postgres. + * Let's use the hint here too. + */ + if (set_pg_ctl_from_PG_CONFIG(pgSetup)) + { + return true; + } + + /* no PG_CONFIG. let's use the more classic approach with PATH instead */ + if (!search_path("pg_ctl", &all_pg_ctls)) + { + return false; + } + + if (!search_path_deduplicate_symlinks(&all_pg_ctls, &pg_ctls)) + { + log_error("Failed to resolve symlinks found in PATH entries, " + "see above for details"); + return false; + } + + if (pg_ctls.found == 1) + { + char *program = pg_ctls.matches[0]; + + strlcpy(pgSetup->pg_ctl, program, MAXPGPATH); + + if (!pg_ctl_version(pgSetup)) + { + log_fatal("Failed to get version info from \"%s\" --version", + pgSetup->pg_ctl); + return false; + } + + log_debug("Found pg_ctl for PostgreSQL %s at \"%s\"", + pgSetup->pg_version, + pgSetup->pg_ctl); + + return true; + } + else + { + /* + * Then, first look for pg_config --bindir with pg_config in PATH, + * we might have a single entry there, as is the case on a typical + * debian/ubuntu packaging, in /usr/bin/pg_config installed from + * the postgresql-common package. + */ + PostgresSetup pgSetupFromPgConfig = { 0 }; + + if (pg_ctls.found == 0) + { + log_debug("Failed to find pg_ctl in PATH, looking for pg_config"); + } + else + { + log_debug("Found %d entries for pg_ctl in PATH, " + "looking for pg_config", + pg_ctls.found); + } + + if (set_pg_ctl_from_pg_config(&pgSetupFromPgConfig)) + { + strlcpy(pgSetup->pg_ctl, + pgSetupFromPgConfig.pg_ctl, + sizeof(pgSetup->pg_ctl)); + + strlcpy(pgSetup->pg_version, + pgSetupFromPgConfig.pg_version, + sizeof(pgSetup->pg_version)); + return true; + } + + /* + * We failed to find a single pg_config in $PATH, error out and + * complain about the situation with enough details that the user + * can understand our struggle in picking a Postgres major version + * for them. + */ + log_info("Found more than one pg_ctl entry in current PATH, " + "and failed to find a single pg_config entry in current PATH"); + + for (int i = 0; i < pg_ctls.found; i++) + { + PostgresSetup currentPgSetup = { 0 }; + + strlcpy(currentPgSetup.pg_ctl, pg_ctls.matches[i], MAXPGPATH); + + if (!pg_ctl_version(¤tPgSetup)) + { + /* + * Because of this it's possible that there's now only a + * single working version of pg_ctl found in PATH. If + * that's the case we will still not use that by default, + * since the users intention is unclear. They might have + * wanted to use the version of pg_ctl that we could not + * parse the version string for. So we warn and continue, + * the user should make their intention clear by using the + * --pg_ctl option (or setting PG_CONFIG, or PATH). + */ + log_warn("Failed to get version info from \"%s\" --version", + currentPgSetup.pg_ctl); + continue; + } + + log_info("Found \"%s\" for pg version %s", + currentPgSetup.pg_ctl, + currentPgSetup.pg_version); + } + + log_error("Found several pg_ctl in PATH, please provide --pgctl"); + + return false; + } +} + + +/* + * find_pg_config_from_pg_ctl finds the path to pg_config from the known path + * to pg_ctl. If that exists, we first use the pg_config binary found in the + * same directory as the pg_ctl binary itself. + * + * Otherwise, we have a look at the PG_CONFIG environment variable. + * + * Finally, we search in the PATH list for all the matches, and for each of + * them we run pg_config --bindir, and if that's the directory where we have + * our known pg_ctl, that's our pg_config. + * + * Rationale: when using debian, the postgresql-common package installs a + * single entry for pg_config in /usr/bin/pg_config, and that's the system + * default. + * + * A version specific file path found in /usr/lib/postgresql/11/bin/pg_config + * when installing Postgres 11 is installed from the package + * postgresql-server-dev-11. + * + * There is no single default entry for pg_ctl, that said, so we are using the + * specific path /usr/lib/postgresql/11/bin/pg_config here. + * + * So depending on what packages have been deployed on this specific debian + * instance, we might or might not find a pg_config binary in the same + * directory as pg_ctl. + * + * Note that we could register the full path to whatever pg_config version we + * use at pg_autoctl create time, but in most cases that is going to be + * /usr/bin/pg_config, and it will point to a new pg_ctl (version 13 for + * instance) when you apt-get upgrade your debian testing distribution and it + * just migrated from Postgres 11 to Postgres 13 (bullseye cycle just did that + * in december 2020). + * + * Either package libpq-dev or postgresql-server-dev-11 (or another version) + * must be isntalled for this to work. + */ +bool +find_pg_config_from_pg_ctl(const char *pg_ctl, char *pg_config, size_t size) +{ + char pg_config_path[MAXPGPATH] = { 0 }; + + /* + * 1. try pg_ctl directory + */ + path_in_same_directory(pg_ctl, "pg_config", pg_config_path); + + if (file_exists(pg_config_path)) + { + log_debug("find_pg_config_from_pg_ctl: \"%s\" " + "in same directory as pg_ctl", + pg_config_path); + strlcpy(pg_config, pg_config_path, size); + return true; + } + + /* + * 2. try PG_CONFIG from the environment, and check pg_config --bindir + */ + if (env_exists("PG_CONFIG")) + { + PostgresSetup pgSetup = { 0 }; + char PG_CONFIG[MAXPGPATH] = { 0 }; + + /* check that the pg_config we found relates to the given pg_ctl */ + if (get_env_copy("PG_CONFIG", PG_CONFIG, sizeof(PG_CONFIG)) && + file_exists(PG_CONFIG) && + set_pg_ctl_from_config_bindir(&pgSetup, PG_CONFIG) && + strcmp(pgSetup.pg_ctl, pg_ctl) == 0) + { + log_debug("find_pg_config_from_pg_ctl: \"%s\" " + "from PG_CONFIG environment variable", + pg_config_path); + strlcpy(pg_config, pg_config_path, size); + return true; + } + } + + /* + * 3. search our PATH for pg_config entries and keep the first one that + * relates to our known pg_ctl. + */ + SearchPath all_pg_configs = { 0 }; + SearchPath pg_configs = { 0 }; + + if (!search_path("pg_config", &all_pg_configs)) + { + return false; + } + + if (!search_path_deduplicate_symlinks(&all_pg_configs, &pg_configs)) + { + log_error("Failed to resolve symlinks found in PATH entries, " + "see above for details"); + return false; + } + + for (int i = 0; i < pg_configs.found; i++) + { + PostgresSetup pgSetup = { 0 }; + + if (set_pg_ctl_from_config_bindir(&pgSetup, pg_configs.matches[i]) && + strcmp(pgSetup.pg_ctl, pg_ctl) == 0) + { + log_debug("find_pg_config_from_pg_ctl: \"%s\" " + "from PATH search", + pg_configs.matches[i]); + strlcpy(pg_config, pg_configs.matches[i], size); + return true; + } + } + + return false; +} + + +/* + * find_extension_control_file ensures that the extension is present in the + * given Postgres installation. This does the equivalent of: + * ls -l $(pg_config --sharedir)/extension/pg_stat_statements.control + */ +bool +find_extension_control_file(const char *pg_ctl, const char *extName) +{ + char pg_config_path[MAXPGPATH] = { 0 }; + char extension_path[MAXPGPATH] = { 0 }; + char *share_dir; + char extension_control_file_name[MAXPGPATH] = { 0 }; + char *lines[1]; + + log_debug("Checking if the %s extension is installed", extName); + + if (!find_pg_config_from_pg_ctl(pg_ctl, pg_config_path, MAXPGPATH)) + { + log_warn("Failed to find pg_config from pg_ctl at \"%s\"", pg_ctl); + return false; + } + + Program prog = run_program(pg_config_path, "--sharedir", NULL); + + if (prog.returnCode == 0) + { + if (!prog.stdOut) + { + log_error("Got empty output from pg_config --sharedir"); + free_program(&prog); + return false; + } + if (splitLines(prog.stdOut, lines, 1) != 1) + { + log_error("Unable to parse output from pg_config --sharedir"); + free_program(&prog); + return false; + } + share_dir = lines[0]; + + join_path_components(extension_path, share_dir, "extension"); + sformat(extension_control_file_name, MAXPGPATH, "%s.control", extName); + join_path_components(extension_path, extension_path, extension_control_file_name); + if (!file_exists(extension_path)) + { + log_error("Failed to find extension control file \"%s\"", + extension_path); + free_program(&prog); + return false; + } + } + else + { + (void) log_program_output(prog, LOG_INFO, LOG_ERROR); + log_error("Failed to run \"%s\", see above for details", + pg_config_path); + free_program(&prog); + return false; + } + free_program(&prog); + return true; +} + + +/* + * pg_add_auto_failover_default_settings ensures the pg_auto_failover default + * settings are included in postgresql.conf. For simplicity, this function + * reads the whole contents of postgresql.conf into memory. + */ +bool +pg_add_auto_failover_default_settings(PostgresSetup *pgSetup, + const char *hostname, + const char *configFilePath, + GUC *settings) +{ + bool includeTuning = true; + char pgAutoFailoverDefaultsConfigPath[MAXPGPATH]; + + /* + * Write the default settings to postgresql-auto-failover.conf. + * + * postgresql-auto-failover.conf needs to be placed alongside + * postgresql.conf for the include to work. Determine the path by finding + * the parent directory of postgresql.conf. + */ + path_in_same_directory(configFilePath, AUTOCTL_DEFAULTS_CONF_FILENAME, + pgAutoFailoverDefaultsConfigPath); + + if (!ensure_default_settings_file_exists(pgAutoFailoverDefaultsConfigPath, + settings, + pgSetup, + hostname, + includeTuning)) + { + return false; + } + + return pg_include_config(configFilePath, + AUTOCTL_CONF_INCLUDE_LINE, + AUTOCTL_CONF_INCLUDE_COMMENT); +} + + +/* + * pg_auto_failover_default_settings_file_exists returns true when our expected + * postgresql-auto-failover.conf file exists in PGDATA. + */ +bool +pg_auto_failover_default_settings_file_exists(PostgresSetup *pgSetup) +{ + char pgAutoFailoverDefaultsConfigPath[MAXPGPATH] = { 0 }; + char *contents = NULL; + long size = 0L; + + + join_path_components(pgAutoFailoverDefaultsConfigPath, + pgSetup->pgdata, + AUTOCTL_DEFAULTS_CONF_FILENAME); + + + /* make sure the file exists and is not empty (race conditions) */ + if (!read_file_if_exists(pgAutoFailoverDefaultsConfigPath, &contents, &size)) + { + return false; + } + + /* we don't actually need the contents here */ + free(contents); + bool fileExistsWithContent = size > 0; + + return fileExistsWithContent; +} + + +/* + * pg_include_config adds an include line to postgresql.conf to include the + * given configuration file, with a comment refering pg_auto_failover. + */ +static bool +pg_include_config(const char *configFilePath, + const char *configIncludeLine, + const char *configIncludeComment) +{ + char *currentConfContents = NULL; + long currentConfSize = 0L; + + /* read the current postgresql.conf contents */ + if (!read_file(configFilePath, ¤tConfContents, ¤tConfSize)) + { + return false; + } + + /* find the include 'postgresql-auto-failover.conf' line */ + char *includeLine = strstr(currentConfContents, configIncludeLine); + + if (includeLine != NULL && (includeLine == currentConfContents || + includeLine[-1] == '\n')) + { + log_debug("%s found in \"%s\"", configIncludeLine, configFilePath); + + /* defaults settings are already included */ + free(currentConfContents); + return true; + } + + log_debug("Adding %s to \"%s\"", configIncludeLine, configFilePath); + + /* build the new postgresql.conf contents */ + PQExpBuffer newConfContents = createPQExpBuffer(); + if (newConfContents == NULL) + { + log_error("Failed to allocate memory"); + free(currentConfContents); + return false; + } + + appendPQExpBufferStr(newConfContents, configIncludeLine); + appendPQExpBufferStr(newConfContents, configIncludeComment); + appendPQExpBufferStr(newConfContents, currentConfContents); + + /* done with the old postgresql.conf contents */ + free(currentConfContents); + + /* memory allocation could have failed while building string */ + if (PQExpBufferBroken(newConfContents)) + { + log_error("Failed to allocate memory"); + destroyPQExpBuffer(newConfContents); + return false; + } + + /* write the new postgresql.conf */ + if (!write_file(newConfContents->data, newConfContents->len, configFilePath)) + { + destroyPQExpBuffer(newConfContents); + return false; + } + + destroyPQExpBuffer(newConfContents); + + return true; +} + + +/* + * ensure_default_settings_file_exists writes the postgresql-auto-failover.conf + * file to the database directory. + */ +static bool +ensure_default_settings_file_exists(const char *configFilePath, + GUC *settings, + PostgresSetup *pgSetup, + const char *hostname, + bool includeTuning) +{ + PQExpBuffer defaultConfContents = createPQExpBuffer(); + + if (defaultConfContents == NULL) + { + log_error("Failed to allocate memory"); + return false; + } + + if (!prepare_guc_settings_from_pgsetup(configFilePath, + defaultConfContents, + settings, + pgSetup, + hostname, + includeTuning)) + { + /* errors have already been logged */ + destroyPQExpBuffer(defaultConfContents); + return false; + } + + if (file_exists(configFilePath)) + { + char *currentDefaultConfContents = NULL; + long currentDefaultConfSize = 0L; + + if (!read_file(configFilePath, ¤tDefaultConfContents, + ¤tDefaultConfSize)) + { + /* technically, we could still try writing, but this is pretty + * suspicious */ + destroyPQExpBuffer(defaultConfContents); + return false; + } + + if (strcmp(currentDefaultConfContents, defaultConfContents->data) == 0) + { + /* file is there and has the same contents, nothing to do */ + log_debug("Default settings file \"%s\" exists", configFilePath); + free(currentDefaultConfContents); + destroyPQExpBuffer(defaultConfContents); + return true; + } + + log_info("Contents of \"%s\" have changed, overwriting", configFilePath); + free(currentDefaultConfContents); + } + else + { + log_debug("Configuration file \"%s\" doesn't exists yet, creating", + configFilePath); + } + + if (!write_file(defaultConfContents->data, + defaultConfContents->len, + configFilePath)) + { + destroyPQExpBuffer(defaultConfContents); + return false; + } + + log_debug("Wrote file \"%s\" with content:\n%s", + configFilePath, defaultConfContents->data); + + destroyPQExpBuffer(defaultConfContents); + + return true; +} + + +/* + * prepare_guc_settings_from_pgsetup replaces some of the given GUC settings + * with dynamic values found in the pgSetup argument, and prepare them in the + * expected format for a postgresql.conf file in the given PQExpBuffer. + * + * While most of our settings are handle in a static way and thus known at + * compile time, some of them can be provided by our users, such as + * listen_addresses, port, and SSL related configuration parameters. + */ + +#define streq(x, y) ((x != NULL) && (y != NULL) && (strcmp(x, y) == 0)) + +static bool +prepare_guc_settings_from_pgsetup(const char *configFilePath, + PQExpBuffer config, + GUC *settings, + PostgresSetup *pgSetup, + const char *hostname, + bool includeTuning) +{ + char tuning[BUFSIZE] = { 0 }; + int settingIndex = 0; + + appendPQExpBufferStr(config, "# Settings by pg_auto_failover\n"); + + /* replace placeholder values with actual pgSetup values */ + for (settingIndex = 0; settings[settingIndex].name != NULL; settingIndex++) + { + GUC *setting = &settings[settingIndex]; + + /* + * Settings for "listen_addresses" and "port" are replaced with the + * respective values present in pgSetup allowing those to be dynamic. + * + * At the moment our "needs quote" heuristic is pretty simple. + * There's the one parameter within those that we hardcode from + * pg_auto_failover that needs quoting, and that's + * listen_addresses. + * + * The reason why POSTGRES_DEFAULT_LISTEN_ADDRESSES is not quoting + * the value directly in the constant is that we are using that + * value both in the configuration file and at the pg_ctl start + * --options "-h *" command line. + * + * At the command line, using --options "-h '*'" would give: + * could not create listen socket for "'*'" + */ + if (streq(setting->name, "listen_addresses")) + { + appendPQExpBuffer(config, "%s = '%s'\n", + setting->name, + pgSetup->listen_addresses); + } + else if (streq(setting->name, "password_encryption")) + { + /* + * Set password_encryption if the authMethod is password based. + */ + if (streq(pgSetup->authMethod, "md5") || + streq(pgSetup->authMethod, "scram-sha-256")) + { + appendPQExpBuffer(config, "%s = '%s'\n", + setting->name, + pgSetup->authMethod); + } + else if (streq(pgSetup->authMethod, "password")) + { + /* + * The "password" auth method supports only the "md5" and + * "scram-sha-256" password encryption settings. + * Default the encryption setting to "scram-sha-256" in this + * case, as it is the more secure alternative. + */ + appendPQExpBuffer(config, "%s = '%s'\n", + setting->name, + "scram-sha-256"); + } + } + else if (streq(setting->name, "port")) + { + appendPQExpBuffer(config, "%s = %d\n", + setting->name, + pgSetup->pgport); + } + else if (streq(setting->name, "ssl")) + { + appendPQExpBuffer(config, "%s = %s\n", + setting->name, + pgSetup->ssl.active == 0 ? "off" : "on"); + } + else if (streq(setting->name, "ssl_ca_file")) + { + if (!IS_EMPTY_STRING_BUFFER(pgSetup->ssl.caFile)) + { + appendPQExpBuffer(config, "%s = '%s'\n", + setting->name, pgSetup->ssl.caFile); + } + } + else if (streq(setting->name, "ssl_crl_file")) + { + if (!IS_EMPTY_STRING_BUFFER(pgSetup->ssl.crlFile)) + { + appendPQExpBuffer(config, "%s = '%s'\n", + setting->name, pgSetup->ssl.crlFile); + } + } + else if (streq(setting->name, "ssl_cert_file")) + { + if (!IS_EMPTY_STRING_BUFFER(pgSetup->ssl.serverCert)) + { + appendPQExpBuffer(config, "%s = '%s'\n", + setting->name, pgSetup->ssl.serverCert); + } + } + else if (streq(setting->name, "ssl_key_file")) + { + if (!IS_EMPTY_STRING_BUFFER(pgSetup->ssl.serverKey)) + { + appendPQExpBuffer(config, "%s = '%s'\n", + setting->name, pgSetup->ssl.serverKey); + } + } + else if (streq(setting->name, "recovery_target_lsn")) + { + if (streq(setting->value, "'immediate'")) + { + appendPQExpBuffer(config, "recovery_target = 'immediate'\n"); + } + else + { + appendPQExpBuffer(config, "%s = %s\n", + setting->name, setting->value); + } + } + else if (streq(setting->name, "citus.node_conninfo")) + { + appendPQExpBuffer(config, "%s = '", setting->name); + + /* add sslmode, sslrootcert, and sslcrl if needed */ + if (!prepare_conninfo_sslmode(config, pgSetup->ssl)) + { + /* errors have already been logged */ + return false; + } + + appendPQExpBufferStr(config, "'\n"); + } + else if (streq(setting->name, "citus.use_secondary_nodes")) + { + if (!IS_EMPTY_STRING_BUFFER(pgSetup->citusClusterName)) + { + appendPQExpBuffer(config, "%s = 'always'\n", setting->name); + } + } + else if (streq(setting->name, "citus.cluster_name")) + { + if (!IS_EMPTY_STRING_BUFFER(pgSetup->citusClusterName)) + { + appendPQExpBuffer(config, "%s = '%s'\n", + setting->name, pgSetup->citusClusterName); + } + } + else if (streq(setting->name, "citus.local_hostname")) + { + if (hostname != NULL && !IS_EMPTY_STRING_BUFFER(hostname)) + { + appendPQExpBuffer(config, "%s = '%s'\n", + setting->name, hostname); + } + } + else if (setting->value != NULL && + !IS_EMPTY_STRING_BUFFER(setting->value)) + { + appendPQExpBuffer(config, "%s = %s\n", + setting->name, + setting->value); + } + else if (setting->value == NULL || + IS_EMPTY_STRING_BUFFER(setting->value)) + { + /* + * Our GUC entry has a NULL (or empty) value. Skip the setting. + * + * In cases that's expected, such as when removing primary_conninfo + * from the recovery.conf settings so that we disconnect from the + * primary node being demoted. + * + * Still log about it, in case it might happen when it's not + * expected. + */ + log_debug("GUC setting \"%s\" has a NULL value", setting->name); + } + else + { + /* the GUC setting in the array has not been processed */ + log_error("BUG: GUC settings \"%s\" has not been processed", + setting->name); + return false; + } + } + + if (includeTuning) + { + if (!pgtuning_prepare_guc_settings(postgres_tuning, + tuning, + sizeof(tuning))) + { + log_warn("Failed to compute Postgres basic tuning for this system"); + } + + appendPQExpBuffer(config, "\n%s\n", tuning); + } + + /* memory allocation could have failed while building string */ + if (PQExpBufferBroken(config)) + { + log_error("Failed to allocate memory while preparing config file \"%s\"", + configFilePath); + destroyPQExpBuffer(config); + return false; + } + + return true; +} + + +bool +ensure_empty_tablespace_dirs(const char *pgdata) +{ + char *dirName = "pg_tblspc"; + struct dirent *dirEntry = NULL; + + char pgTblspcFullPath[MAXPGPATH] = { 0 }; + sformat(pgTblspcFullPath, MAXPGPATH, "%s/%s", pgdata, dirName); + + if (!directory_exists(pgTblspcFullPath)) + { + log_debug("Postgres dir pg_tblspc does not exist at \"%s\"", + pgTblspcFullPath); + return true; + } + + + /* open and scan through the Postgres tablespace directory */ + DIR *tblspcDir = opendir(pgTblspcFullPath); + + if (tblspcDir == NULL) + { + log_error("Failed to open Postgres tablespace directory \"%s\" at \"%s\"", + dirName, pgdata); + return false; + } + + while ((dirEntry = readdir(tblspcDir)) != NULL) + { + char tblspcDataDirPath[MAXPGPATH] = { 0 }; + + /* skip non-symlinks, as all tablespace dirs are symlinks */ + if (dirEntry->d_type == DT_UNKNOWN) + { + struct stat structStat; + char dirEntryFullPath[MAXPGPATH] = { 0 }; + sformat(dirEntryFullPath, MAXPGPATH, "%s/%s", pgTblspcFullPath, + dirEntry->d_name); + if (lstat(dirEntryFullPath, &structStat) != 0) + { + log_error("Failed to get file information for \"%s/%s\": %m", + pgTblspcFullPath, dirEntry->d_name); + return false; + } + if (!S_ISLNK(structStat.st_mode)) + { + log_debug("Non-symlink file found in tablespace directory: \"%s/%s\"", + pgTblspcFullPath, dirEntry->d_name); + continue; + } + } + else if (dirEntry->d_type != DT_LNK) + { + log_debug("Non-symlink file found in tablespace directory: \"%s/%s\"", + pgTblspcFullPath, dirEntry->d_name); + continue; + } + + join_path_components(tblspcDataDirPath, pgTblspcFullPath, dirEntry->d_name); + + log_debug("Removing contents of tablespace data directory \"%s\"", + tblspcDataDirPath); + + /* remove contents of tablespace data directory */ + if (!rmtree(tblspcDataDirPath, false)) + { + log_error( + "Failed to remove contents of existing tablespace data directory \"%s\": %m", + tblspcDataDirPath); + return false; + } + } + + return true; +} + + +/* + * Call pg_basebackup, using a temporary directory for the duration of the data + * transfer. + */ +bool +pg_basebackup(const char *pgdata, + const char *pg_ctl, + ReplicationSource *replicationSource) +{ + int returnCode; + char pg_basebackup[MAXPGPATH]; + + NodeAddress *primaryNode = &(replicationSource->primaryNode); + char primaryConnInfo[MAXCONNINFO] = { 0 }; + + char *args[18]; /* enough for all pg_basebackup flags incl. --checkpoint=fast */ + int argsIndex = 0; + + char command[BUFSIZE]; + char pgpassword[BUFSIZE] = { 0 }; + + log_debug("mkdir -p \"%s\"", replicationSource->backupDir); + if (!ensure_empty_dir(replicationSource->backupDir, 0700)) + { + /* errors have already been logged. */ + return false; + } + + if (!ensure_empty_tablespace_dirs(pgdata)) + { + /* errors have already been logged. */ + return false; + } + + /* call pg_basebackup */ + path_in_same_directory(pg_ctl, "pg_basebackup", pg_basebackup); + + setenv("PGCONNECT_TIMEOUT", POSTGRES_CONNECT_TIMEOUT, 1); + + if (!IS_EMPTY_STRING_BUFFER(replicationSource->password)) + { + if (env_exists("PGPASSWORD")) + { + if (!get_env_copy("PGPASSWORD", pgpassword, sizeof(pgpassword))) + { + /* errors have already been logged. */ + return false; + } + } + setenv("PGPASSWORD", replicationSource->password, 1); + } + setenv("PGAPPNAME", replicationSource->applicationName, 1); + + if (!prepare_primary_conninfo(primaryConnInfo, + MAXCONNINFO, + primaryNode->host, + primaryNode->port, + replicationSource->userName, + NULL, /* no database */ + NULL, /* no password here */ + replicationSource->applicationName, + replicationSource->sslOptions, + false)) /* do not escape this one */ + { + /* errors have already been logged. */ + return false; + } + + args[argsIndex++] = (char *) pg_basebackup; + args[argsIndex++] = "-w"; + args[argsIndex++] = "-d"; + args[argsIndex++] = primaryConnInfo; + args[argsIndex++] = "--pgdata"; + args[argsIndex++] = replicationSource->backupDir; + args[argsIndex++] = "-U"; + args[argsIndex++] = replicationSource->userName; + args[argsIndex++] = "--verbose"; + args[argsIndex++] = "--progress"; + args[argsIndex++] = "--max-rate"; + args[argsIndex++] = replicationSource->maximumBackupRate; + args[argsIndex++] = "--wal-method=stream"; + args[argsIndex++] = "--checkpoint=fast"; + + /* we don't use a replication slot e.g. when upstream is a standby */ + if (!IS_EMPTY_STRING_BUFFER(replicationSource->slotName)) + { + args[argsIndex++] = "--slot"; + args[argsIndex++] = replicationSource->slotName; + } + + args[argsIndex] = NULL; + + /* + * We do not want to call setsid() when running this program, as the + * pg_basebackup subprogram is not intended to be its own session leader, + * but remain a sub-process in the same group as pg_autoctl. + */ + Program program = { 0 }; + + (void) initialize_program(&program, args, false); + program.processBuffer = &processBufferCallback; + + /* log the exact command line we're using */ + int commandSize = snprintf_program_command_line(&program, command, BUFSIZE); + + if (commandSize >= BUFSIZE) + { + /* we only display the first BUFSIZE bytes of the real command */ + log_info("%s...", command); + } + else + { + log_info("%s", command); + } + + (void) execute_subprogram(&program); + + /* clean-up the environment again */ + if (!IS_EMPTY_STRING_BUFFER(replicationSource->password)) + { + if (IS_EMPTY_STRING_BUFFER(pgpassword)) + { + unsetenv("PGPASSWORD"); + } + else + { + setenv("PGPASSWORD", pgpassword, 1); + } + } + + returnCode = program.returnCode; + free_program(&program); + + if (returnCode != 0) + { + log_error("Failed to run pg_basebackup: exit code %d", returnCode); + return false; + } + + /* replace $pgdata with the backup directory */ + if (directory_exists(pgdata)) + { + if (!rmtree(pgdata, true)) + { + log_error("Failed to remove directory \"%s\": %m", pgdata); + return false; + } + } + + log_debug("mv \"%s\" \"%s\"", replicationSource->backupDir, pgdata); + + if (rename(replicationSource->backupDir, pgdata) != 0) + { + log_error( + "Failed to install pg_basebackup dir " " \"%s\" in \"%s\": %m", + replicationSource->backupDir, pgdata); + return false; + } + + return true; +} + + +/* + * pg_rewind runs the pg_rewind program to rewind the given database directory + * to a state where it can follow the given primary. We need the ability to + * connect to the node. + */ +bool +pg_rewind(const char *pgdata, + const char *pg_ctl, + ReplicationSource *replicationSource) +{ + int returnCode; + char pg_rewind[MAXPGPATH] = { 0 }; + + NodeAddress *primaryNode = &(replicationSource->primaryNode); + char primaryConnInfo[MAXCONNINFO] = { 0 }; + + char *args[7]; + int argsIndex = 0; + + char command[BUFSIZE]; + char pgpassword[BUFSIZE] = { 0 }; + + /* call pg_rewind*/ + path_in_same_directory(pg_ctl, "pg_rewind", pg_rewind); + + setenv("PGCONNECT_TIMEOUT", POSTGRES_CONNECT_TIMEOUT, 1); + + if (!IS_EMPTY_STRING_BUFFER(replicationSource->password)) + { + if (env_exists("PGPASSWORD")) + { + if (!get_env_copy("PGPASSWORD", pgpassword, sizeof(pgpassword))) + { + /* errors have already been logged. */ + return false; + } + } + setenv("PGPASSWORD", replicationSource->password, 1); + } + + if (!prepare_primary_conninfo(primaryConnInfo, + MAXCONNINFO, + primaryNode->host, + primaryNode->port, + replicationSource->userName, + "postgres", /* pg_rewind needs a database */ + NULL, /* no password here */ + replicationSource->applicationName, + replicationSource->sslOptions, + false)) /* do not escape this one */ + { + /* errors have already been logged. */ + return false; + } + + args[argsIndex++] = (char *) pg_rewind; + args[argsIndex++] = "--target-pgdata"; + args[argsIndex++] = (char *) pgdata; + args[argsIndex++] = "--source-server"; + args[argsIndex++] = primaryConnInfo; + args[argsIndex++] = "--progress"; + args[argsIndex] = NULL; + + /* + * We do not want to call setsid() when running this program, as the + * pg_rewind subprogram is not intended to be its own session leader, but + * remain a sub-process in the same group as pg_autoctl. + */ + Program program = { 0 }; + + (void) initialize_program(&program, args, false); + program.processBuffer = &processBufferCallback; + + /* log the exact command line we're using */ + int commandSize = snprintf_program_command_line(&program, command, BUFSIZE); + + if (commandSize >= BUFSIZE) + { + /* we only display the first BUFSIZE bytes of the real command */ + log_info("%s...", command); + } + else + { + log_info("%s", command); + } + + (void) execute_subprogram(&program); + + /* clean-up the environment again */ + if (!IS_EMPTY_STRING_BUFFER(replicationSource->password)) + { + if (IS_EMPTY_STRING_BUFFER(pgpassword)) + { + unsetenv("PGPASSWORD"); + } + else + { + setenv("PGPASSWORD", pgpassword, 1); + } + } + + returnCode = program.returnCode; + free_program(&program); + + if (returnCode != 0) + { + log_error("Failed to run pg_rewind: exit code %d", returnCode); + return false; + } + + return true; +} + + +/* log_program_output logs the output of the given program. */ +static void +log_program_output(Program prog, int outLogLevel, int errorLogLevel) +{ + if (prog.stdOut != NULL) + { + char *outLines[BUFSIZE]; + int lineCount = splitLines(prog.stdOut, outLines, BUFSIZE); + int lineNumber = 0; + + for (lineNumber = 0; lineNumber < lineCount; lineNumber++) + { + log_level(outLogLevel, "%s", outLines[lineNumber]); + } + } + + if (prog.stdErr != NULL) + { + char *errorLines[BUFSIZE]; + int lineCount = splitLines(prog.stdErr, errorLines, BUFSIZE); + int lineNumber = 0; + + for (lineNumber = 0; lineNumber < lineCount; lineNumber++) + { + log_level(errorLogLevel, "%s", errorLines[lineNumber]); + } + } +} + + +/* + * pg_ctl_initdb initializes a PostgreSQL directory from scratch by calling + * "pg_ctl initdb", and returns true when this was successful. Beware that it + * will inherit from the environment, such as LC_COLLATE and LC_ALL etc. + * + * No provision is made to control (sanitize?) that environment. + */ +bool +pg_ctl_initdb(const char *pg_ctl, const char *pgdata) +{ + /* initdb takes time, so log about the operation BEFORE doing it */ + log_info("Initialising a PostgreSQL cluster at \"%s\"", pgdata); + log_info("%s initdb -s -D %s --option '--auth=trust'", pg_ctl, pgdata); + + Program program = run_program(pg_ctl, + "--silent", + "--pgdata", pgdata, + + /* avoid warning message */ + "--option", "'--auth=trust'", "initdb", + NULL); + + bool success = program.returnCode == 0; + + if (program.returnCode != 0) + { + (void) log_program_output(program, LOG_INFO, LOG_ERROR); + log_fatal("Failed to initialize Postgres cluster at \"%s\", " + "see above for details", + pgdata); + } + else + { + /* we might still have important information to read there */ + (void) log_program_output(program, LOG_INFO, LOG_WARN); + } + free_program(&program); + + return success; +} + + +/* + * pg_ctl_postgres runs the "postgres" command-line in the current process, + * with the same options as we would use in pg_ctl_start. pg_ctl_postgres does + * not fork a Postgres process in the background, we keep the control over the + * postmaster process. Think exec() rather then fork(). + * + * This function will take over the current standard output and standard error + * file descriptor, closing them and then giving control to them to Postgres + * itself. This function is meant to be called in the child process of a fork() + * call done by the caller. + */ +bool +pg_ctl_postgres(const char *pg_ctl, const char *pgdata, int pgport, + char *listen_addresses, bool listen) +{ + char postgres[MAXPGPATH]; + char logfile[MAXPGPATH]; + + char *args[12]; + int argsIndex = 0; + + char env_pg_regress_sock_dir[MAXPGPATH]; + + char command[BUFSIZE]; + + /* call postgres directly */ + path_in_same_directory(pg_ctl, "postgres", postgres); + + /* prepare startup.log file in PGDATA */ + join_path_components(logfile, pgdata, "startup.log"); + + args[argsIndex++] = (char *) postgres; + args[argsIndex++] = "-D"; + args[argsIndex++] = (char *) pgdata; + args[argsIndex++] = "-p"; + IntString pgportStr = intToString(pgport); + args[argsIndex++] = (char *) pgportStr.strValue; + + if (listen) + { + if (IS_EMPTY_STRING_BUFFER(listen_addresses)) + { + log_error("BUG: pg_ctl_postgres is given an empty listen_addresses " + "with argument listen set to true"); + return false; + } + args[argsIndex++] = "-h"; + args[argsIndex++] = (char *) listen_addresses; + } + else + { + args[argsIndex++] = "-h"; + args[argsIndex++] = ""; + } + + if (env_exists("PG_REGRESS_SOCK_DIR")) + { + if (!get_env_copy("PG_REGRESS_SOCK_DIR", env_pg_regress_sock_dir, + MAXPGPATH)) + { + /* errors have already been logged */ + return false; + } + + args[argsIndex++] = "-k"; + args[argsIndex++] = (char *) env_pg_regress_sock_dir; + } + + args[argsIndex] = NULL; + + /* + * We do not want to call setsid() when running this program, as the + * postgres subprogram is not intended to be its own session leader, but + * remain a sub-process in the same group as pg_autoctl. + */ + Program program = { 0 }; + + (void) initialize_program(&program, args, false); + + /* we want to redirect the output to logfile */ + int logFileDescriptor = open(logfile, FOPEN_FLAGS_W, 0644); + + if (logFileDescriptor == -1) + { + log_error("Failed to open file \"%s\": %m", logfile); + } + + program.capture = false; /* redirect output, don't capture */ + program.stdOutFd = logFileDescriptor; + program.stdErrFd = logFileDescriptor; + + /* log the exact command line we're using */ + int commandSize = snprintf_program_command_line(&program, command, BUFSIZE); + + if (commandSize >= BUFSIZE) + { + /* we only display the first BUFSIZE bytes of the real command */ + log_info("%s...", command); + } + else + { + log_info("%s", command); + } + + (void) execute_program(&program); + + return program.returnCode == 0; +} + + +/* + * pg_log_startup logs the PGDATA/startup.log file contents so that our users + * have enough information about why Postgres failed to start when that + * happens. + */ +bool +pg_log_startup(const char *pgdata, int logLevel) +{ + char pgLogDirPath[MAXPGPATH] = { 0 }; + + char pgStartupPath[MAXPGPATH] = { 0 }; + char *fileContents; + long fileSize; + + /* logLevel to use when introducing which file path logs come from */ + int pathLogLevel = logLevel <= LOG_DEBUG ? LOG_DEBUG : LOG_WARN; + + struct stat pgStartupStat; + + struct dirent *logFileDirEntry = NULL; + + /* prepare startup.log file in PGDATA */ + join_path_components(pgStartupPath, pgdata, "startup.log"); + + if (read_file(pgStartupPath, &fileContents, &fileSize) && fileSize > 0) + { + char *lines[BUFSIZE]; + int lineCount = splitLines(fileContents, lines, BUFSIZE); + int lineNumber = 0; + + log_level(pathLogLevel, "Postgres logs from \"%s\":", pgStartupPath); + + for (lineNumber = 0; lineNumber < lineCount; lineNumber++) + { + log_level(logLevel, "%s", lines[lineNumber]); + } + + free(fileContents); + } + + /* + * Add in the most recent Postgres log file if it's been created after the + * startup.log file, it might contain very useful information, such as a + * FATAL line(s). + * + * Given that we setup Postgres to use the logging_collector, we expect + * there to be a single Postgres log file in the "log" directory that was + * created later than the "startup.log" file, and we expect the file to be + * rather short. + * + * Also we setup log_directory to be "log" so that's where we are looking + * into. + */ + + /* prepare PGDATA/log directory path */ + join_path_components(pgLogDirPath, pgdata, "log"); + + if (!directory_exists(pgLogDirPath)) + { + /* then there's no other log files to process here */ + return true; + } + + /* get the time of last modification of the startup.log file */ + if (lstat(pgStartupPath, &pgStartupStat) != 0) + { + log_error("Failed to get file information for \"%s\": %m", + pgStartupPath); + return false; + } + int64_t pgStartupMtime = ST_MTIME_S(pgStartupStat); + + /* open and scan through the Postgres log directory */ + DIR *logDir = opendir(pgLogDirPath); + + if (logDir == NULL) + { + log_error("Failed to open Postgres log directory \"%s\": %m", + pgLogDirPath); + return false; + } + + while ((logFileDirEntry = readdir(logDir)) != NULL) + { + char pgLogFilePath[MAXPGPATH] = { 0 }; + struct stat pgLogFileStat; + + + /* build the absolute file path for the logfile */ + join_path_components(pgLogFilePath, + pgLogDirPath, + logFileDirEntry->d_name); + + /* get the file information for the current logFile */ + if (lstat(pgLogFilePath, &pgLogFileStat) != 0) + { + log_error("Failed to get file information for \"%s\": %m", + pgLogFilePath); + return false; + } + + /* + * our logFiles are regular files, skip . and .. and others + * first, check for systems that do not handle d_type, and skip non-regular types + */ + if (logFileDirEntry->d_type == DT_UNKNOWN) + { + if (!S_ISREG(pgLogFileStat.st_mode)) + { + continue; + } + } + + /* + * next, ignore all other non-regular types + * (if this check were first, we would skip all with DT_UNKNOWN) + */ + else if (logFileDirEntry->d_type != DT_REG) + { + continue; + } + + int64_t pgLogFileMtime = ST_MTIME_S(pgLogFileStat); + + /* + * Compare modification times and only add to our logs the content + * from the Postgres log file that was created after the + * startup.log file. + */ + if (pgLogFileMtime >= pgStartupMtime) + { + log_level(pathLogLevel, + "Postgres logs from \"%s\":", pgLogFilePath); + + if (read_file(pgLogFilePath, &fileContents, &fileSize) && + fileSize > 0) + { + char *lines[BUFSIZE]; + int lineCount = splitLines(fileContents, lines, BUFSIZE); + int lineNumber = 0; + + for (lineNumber = 0; lineNumber < lineCount; lineNumber++) + { + if (strstr(lines[lineNumber], "FATAL") != NULL) + { + log_fatal("%s", lines[lineNumber]); + } + else if (strstr(lines[lineNumber], "ERROR") != NULL) + { + log_error("%s", lines[lineNumber]); + } + else + { + log_level(logLevel, "%s", lines[lineNumber]); + } + } + + free(fileContents); + } + } + } + + closedir(logDir); + + /* now add the contents of the recovery configuration */ + (void) pg_log_recovery_setup(pgdata, logLevel); + + return true; +} + + +/* + * pg_log_recovery_setup logs the current Postgres recovery settings from + * either the recovery.conf file or the standby setup. In case things go wrong + * in the Postgres version detection mechanism, or upgrades, or clean-up, this + * logs all the configuration files found rather than only those we expect we + * should find. + */ +bool +pg_log_recovery_setup(const char *pgdata, int logLevel) +{ + char *filenames[] = { + "recovery.conf", + "standby.signal", + AUTOCTL_STANDBY_CONF_FILENAME, + NULL + }; + + for (int i = 0; filenames[i] != NULL; i++) + { + char recoveryConfPath[MAXPGPATH] = { 0 }; + char *fileContents; + long fileSize; + + join_path_components(recoveryConfPath, pgdata, filenames[i]); + + if (file_exists(recoveryConfPath)) + { + if (!read_file(recoveryConfPath, &fileContents, &fileSize)) + { + /* errors have already been logged */ + continue; + } + + if (fileSize > 0) + { + log_debug("Configuration file \"%s\":\n%s", + recoveryConfPath, fileContents); + } + else + { + log_debug("Configuration file \"%s\" is empty", + recoveryConfPath); + } + + free(fileContents); + } + } + + return true; +} + + +/* + * pg_ctl_stop tries to stop a PostgreSQL server by running a "pg_ctl stop" + * command. If the server was stopped successfully, or if the server is not + * running at all, it returns true. + */ +bool +pg_ctl_stop(const char *pg_ctl, const char *pgdata) +{ + const bool log_output = true; + + log_info("%s --pgdata %s --wait stop --mode fast", pg_ctl, pgdata); + + Program program = run_program(pg_ctl, + "--pgdata", pgdata, + "--wait", + "--mode", "fast", + "stop", + NULL); + + /* + * Case 1. "pg_ctl stop" was successful, so we could stop the PostgreSQL + * server successfully. + */ + if (program.returnCode == 0) + { + free_program(&program); + return true; + } + + /* + * Case 2. The data directory doesn't exist. So we assume PostgreSQL is + * not running, so stopping the PostgreSQL server was successful. + */ + bool pgdata_exists = directory_exists(pgdata); + if (!pgdata_exists) + { + log_info("pgdata \"%s\" does not exist, consider this as PostgreSQL " + "not running", pgdata); + free_program(&program); + return true; + } + + /* + * Case 3. "pg_ctl stop" returns non-zero return code when PostgreSQL is not + * running at all. So we double-check with "pg_ctl status", and return + * success if the PostgreSQL server is not running. Otherwise, we return + * failure. + * + * See https://www.postgresql.org/docs/current/static/app-pg-ctl.html + */ + + int status = pg_ctl_status(pg_ctl, pgdata, log_output); + if (status == PG_CTL_STATUS_NOT_RUNNING) + { + log_info("pg_ctl stop failed, but PostgreSQL is not running anyway"); + free_program(&program); + return true; + } + + log_info("Stopping PostgreSQL server failed. pg_ctl status returned: %d", + status); + + if (log_output) + { + (void) log_program_output(program, LOG_INFO, LOG_ERROR); + } + + free_program(&program); + return false; +} + + +/* + * pg_ctl_status gets the status of the PostgreSQL server by running + * "pg_ctl status". Output of this command is logged if log_output is true. + * Return code of this command is returned. + */ +int +pg_ctl_status(const char *pg_ctl, const char *pgdata, bool log_output) +{ + Program program = run_program(pg_ctl, "-D", pgdata, "status", NULL); + int returnCode = program.returnCode; + + log_level(log_output ? LOG_INFO : LOG_DEBUG, + "%s status -D %s [%d]", pg_ctl, pgdata, returnCode); + + if (log_output) + { + (void) log_program_output(program, LOG_INFO, LOG_ERROR); + } + + free_program(&program); + return returnCode; +} + + +/* + * pg_ctl_promote promotes a standby by running "pg_ctl promote" + */ +bool +pg_ctl_promote(const char *pg_ctl, const char *pgdata) +{ + Program program = + run_program(pg_ctl, "-D", pgdata, "--no-wait", "promote", NULL); + int returnCode = program.returnCode; + + log_debug("%s promote -D %s --no-wait", pg_ctl, pgdata); + + if (program.stdErr != NULL) + { + log_error("%s", program.stdErr); + } + + if (returnCode != 0) + { + /* pg_ctl promote will have logged errors */ + free_program(&program); + return false; + } + + free_program(&program); + return true; +} + + +/* + * pg_ctl_reload reloads Postgres configuration by running "pg_ctl reload". + * Does not require a libpq connection — useful when HBA hasn't been set up yet. + */ +bool +pg_ctl_reload(const char *pg_ctl, const char *pgdata) +{ + Program program = run_program(pg_ctl, "-D", pgdata, "reload", NULL); + int returnCode = program.returnCode; + + if (program.stdErr != NULL) + { + log_debug("%s", program.stdErr); + } + + free_program(&program); + + if (returnCode != 0) + { + log_error("pg_ctl reload -D %s failed (exit %d)", pgdata, returnCode); + return false; + } + + return true; +} + + +/* + * pg_setup_standby_mode sets up standby mode by either writing a recovery.conf + * file or adding the configuration items to postgresql.conf and then creating + * a standby.signal file in PGDATA. + */ +bool +pg_setup_standby_mode(uint32_t pg_control_version, + const char *pgdata, + const char *pg_ctl, + ReplicationSource *replicationSource) +{ + if (pg_control_version < 1000) + { + log_fatal("pg_auto_failover does not support PostgreSQL before " + "Postgres 10, we have pg_control version number %d from " + "pg_controldata \"%s\"", + pg_control_version, pgdata); + return false; + } + + /* + * Check our primary_conninfo connection string by attempting to connect in + * replication mode and issuing a IDENTIFY_SYSTEM command. + */ + if (!IS_EMPTY_STRING_BUFFER(replicationSource->primaryNode.host) && + !pgctl_identify_system(replicationSource)) + { + log_error("Failed to setup standby mode: can't connect to the primary. " + "See above for details"); + return false; + } + + if (pg_control_version < 1200) + { + /* + * Before Postgres 12 we used to place recovery configuration in a + * specific file recovery.conf, located alongside postgresql.conf. + * Controling whether the server would start in PITR or standby mode + * was controlled by a setting in the recovery.conf file. + */ + return pg_write_recovery_conf(pgdata, replicationSource); + } + else + { + /* + * Starting in Postgres 12 we need to add our recovery configuration to + * the main postgresql.conf file and create an empty standby.signal + * file to trigger starting the server in standby mode. + */ + return pg_write_standby_signal(pgdata, replicationSource); + } +} + + +/* + * pg_write_recovery_conf writes a recovery.conf file to a postgres data + * directory with the given primary connection info and replication slot name. + */ +static bool +pg_write_recovery_conf(const char *pgdata, ReplicationSource *replicationSource) +{ + char recoveryConfPath[MAXPGPATH]; + + /* prepare storage areas for parameters */ + char primaryConnInfo[MAXCONNINFO] = { 0 }; + char primarySlotName[MAXCONNINFO] = { 0 }; + char targetLSN[PG_LSN_MAXLENGTH] = { 0 }; + char targetAction[NAMEDATALEN] = { 0 }; + char targetTimeline[NAMEDATALEN] = { 0 }; + + GUC recoverySettingsStandby[] = { + { "standby_mode", "'on'" }, + { "primary_conninfo", (char *) primaryConnInfo }, + { "primary_slot_name", (char *) primarySlotName }, + { "recovery_target_timeline", (char *) targetTimeline }, + { NULL, NULL } + }; + + GUC recoverySettingsTargetLSN[] = { + { "standby_mode", "'on'" }, + { "primary_conninfo", (char *) primaryConnInfo }, + { "primary_slot_name", (char *) primarySlotName }, + { "recovery_target_timeline", (char *) targetTimeline }, + { "recovery_target_lsn", (char *) targetLSN }, + { "recovery_target_inclusive", "'true'" }, + { "recovery_target_action", (char *) targetAction }, + { NULL, NULL } + }; + + GUC *recoverySettings = + IS_EMPTY_STRING_BUFFER(replicationSource->targetLSN) + ? recoverySettingsStandby + : recoverySettingsTargetLSN; + + bool includeTuning = false; + + join_path_components(recoveryConfPath, pgdata, "recovery.conf"); + + log_info("Writing recovery configuration to \"%s\"", recoveryConfPath); + + if (!prepare_recovery_settings(pgdata, + replicationSource, + primaryConnInfo, + primarySlotName, + targetLSN, + targetAction, + targetTimeline)) + { + /* errors have already been logged */ + return false; + } + + return ensure_default_settings_file_exists(recoveryConfPath, + recoverySettings, + NULL, + NULL, + includeTuning); +} + + +/* + * pg_write_standby_signal writes the ${PGDATA}/standby.signal file that is in + * use starting with Postgres 12 for starting a standby server. The file only + * needs to exists, and the setup is to be found in the main Postgres + * configuration file. + */ +static bool +pg_write_standby_signal(const char *pgdata, + ReplicationSource *replicationSource) +{ + char standbyConfigFilePath[MAXPGPATH] = { 0 }; + char signalFilePath[MAXPGPATH] = { 0 }; + char configFilePath[MAXPGPATH] = { 0 }; + + /* prepare storage areas for parameters */ + char primaryConnInfo[MAXCONNINFO] = { 0 }; + char primarySlotName[MAXCONNINFO] = { 0 }; + char targetLSN[PG_LSN_MAXLENGTH] = { 0 }; + char targetAction[NAMEDATALEN] = { 0 }; + char targetTimeline[NAMEDATALEN] = { 0 }; + + GUC recoverySettingsStandby[] = { + { "primary_conninfo", (char *) primaryConnInfo }, + { "primary_slot_name", (char *) primarySlotName }, + { "recovery_target_timeline", (char *) targetTimeline }, + { NULL, NULL } + }; + + GUC recoverySettingsTargetLSN[] = { + { "primary_conninfo", (char *) primaryConnInfo }, + { "primary_slot_name", (char *) primarySlotName }, + { "recovery_target_timeline", (char *) targetTimeline }, + { "recovery_target_lsn", (char *) targetLSN }, + { "recovery_target_inclusive", "'true'" }, + { "recovery_target_action", targetAction }, + { NULL, NULL } + }; + + GUC *recoverySettings = + IS_EMPTY_STRING_BUFFER(replicationSource->targetLSN) + ? recoverySettingsStandby + : recoverySettingsTargetLSN; + + bool includeTuning = false; + + log_trace("pg_write_standby_signal"); + + if (!prepare_recovery_settings(pgdata, + replicationSource, + primaryConnInfo, + primarySlotName, + targetLSN, + targetAction, + targetTimeline)) + { + /* errors have already been logged */ + return false; + } + + /* set our configuration file paths, all found in PGDATA */ + join_path_components(signalFilePath, pgdata, "standby.signal"); + join_path_components(configFilePath, pgdata, "postgresql.conf"); + join_path_components(standbyConfigFilePath, + pgdata, + AUTOCTL_STANDBY_CONF_FILENAME); + + /* + * First install the standby.signal file, so that if there's a problem + * later and Postgres is started, it is started as a standby, with missing + * configuration. + */ + + /* only logs about this the first time */ + if (!file_exists(signalFilePath)) + { + log_info("Creating the standby signal file at \"%s\", " + "and replication setup at \"%s\"", + signalFilePath, standbyConfigFilePath); + } + + if (!write_file("", 0, signalFilePath)) + { + /* write_file logs I/O error */ + return false; + } + + /* + * Now write the standby settings to postgresql-auto-failover-standby.conf + * and include that file from postgresql.conf. + * + * we pass NULL as pgSetup because we know it won't be used... + */ + if (!ensure_default_settings_file_exists(standbyConfigFilePath, + recoverySettings, + NULL, + NULL, + includeTuning)) + { + return false; + } + + /* + * We successfully created the standby.signal file, so Postgres will start + * as a standby. If we fail to install the standby settings, then we return + * false here and let the main loop try again. At least Postgres won't + * start as a cloned single accepting writes. + */ + if (!pg_include_config(configFilePath, + AUTOCTL_SB_CONF_INCLUDE_LINE, + AUTOCTL_CONF_INCLUDE_COMMENT)) + { + log_error("Failed to prepare \"%s\" with standby settings", + standbyConfigFilePath); + return false; + } + + return true; +} + + +/* + * prepare_recovery_settings prepares the settings that we need to install in + * either recovery.conf or our own postgresql-auto-failover-standby.conf + * depending on the Postgres major version. + */ +static bool +prepare_recovery_settings(const char *pgdata, + ReplicationSource *replicationSource, + char *primaryConnInfo, + char *primarySlotName, + char *targetLSN, + char *targetAction, + char *targetTimeline) +{ + bool escape = true; + NodeAddress *primaryNode = &(replicationSource->primaryNode); + + /* when reaching REPORT_LSN we set recovery with no primary conninfo */ + if (!IS_EMPTY_STRING_BUFFER(primaryNode->host)) + { + log_debug("prepare_recovery_settings: " + "primary node %" PRId64 " \"%s\" (%s:%d)", + primaryNode->nodeId, + primaryNode->name, + primaryNode->host, + primaryNode->port); + + if (!prepare_primary_conninfo(primaryConnInfo, + MAXCONNINFO, + primaryNode->host, + primaryNode->port, + replicationSource->userName, + NULL, /* no database */ + replicationSource->password, + replicationSource->applicationName, + replicationSource->sslOptions, + escape)) + { + /* errors have already been logged. */ + return false; + } + } + else + { + log_debug("prepare_recovery_settings: no primary node!"); + } + + /* + * We don't always have a replication slot name to use when connecting to a + * standby node. + */ + if (!IS_EMPTY_STRING_BUFFER(replicationSource->slotName)) + { + sformat(primarySlotName, MAXCONNINFO, "'%s'", + replicationSource->slotName); + } + + /* The default target timeline is 'latest' */ + if (IS_EMPTY_STRING_BUFFER(replicationSource->targetTimeline)) + { + sformat(targetTimeline, NAMEDATALEN, "'latest'"); + } + else + { + sformat(targetTimeline, NAMEDATALEN, "'%s'", + replicationSource->targetTimeline); + } + + /* We use the targetLSN only when doing a WAL fast_forward operation */ + if (!IS_EMPTY_STRING_BUFFER(replicationSource->targetLSN)) + { + sformat(targetLSN, PG_LSN_MAXLENGTH, "'%s'", + replicationSource->targetLSN); + } + + /* The default target Action is 'pause' */ + if (IS_EMPTY_STRING_BUFFER(replicationSource->targetAction)) + { + sformat(targetAction, NAMEDATALEN, "'pause'"); + } + else + { + sformat(targetAction, NAMEDATALEN, "'%s'", + replicationSource->targetAction); + } + + return true; +} + + +/* + * pg_cleanup_standby_mode cleans-up the replication settings for the local + * instance of Postgres found at pgdata. + * + * - remove either recovery.conf or standby.signal + * + * - when using Postgres 12 also make postgresql-auto-failover-standby.conf an + * empty file, so that we can still include it, but it has no effect. + */ +bool +pg_cleanup_standby_mode(uint32_t pg_control_version, + const char *pg_ctl, + const char *pgdata, + PGSQL *pgsql) +{ + if (pg_control_version < 1200) + { + char recoveryConfPath[MAXPGPATH]; + + join_path_components(recoveryConfPath, pgdata, "recovery.conf"); + + log_debug("pg_cleanup_standby_mode: rm \"%s\"", recoveryConfPath); + + if (!unlink_file(recoveryConfPath)) + { + /* errors have already been logged */ + return false; + } + } + else + { + char standbyConfigFilePath[MAXPGPATH]; + char signalFilePath[MAXPGPATH]; + + join_path_components(signalFilePath, pgdata, "standby.signal"); + join_path_components(standbyConfigFilePath, + pgdata, + AUTOCTL_STANDBY_CONF_FILENAME); + + log_debug("pg_cleanup_standby_mode: rm \"%s\"", signalFilePath); + + if (!unlink_file(signalFilePath)) + { + /* errors have already been logged */ + return false; + } + + /* empty out the standby configuration file */ + log_debug("pg_cleanup_standby_mode: > \"%s\"", standbyConfigFilePath); + + if (!write_file("", 0, standbyConfigFilePath)) + { + /* write_file logs I/O error */ + return false; + } + } + + return true; +} + + +/* + * escape_recovery_conf_string escapes a string that is used in a recovery.conf + * file by converting single quotes into two single quotes. + * + * The result is written to destination and the length of the result. + */ +static bool +escape_recovery_conf_string(char *destination, int destinationSize, + const char *recoveryConfString) +{ + int charIndex = 0; + int length = strlen(recoveryConfString); + int escapedStringLength = 0; + + /* we are going to add at least 3 chars: two quotes and a NUL character */ + if (destinationSize < (length + 3)) + { + log_error("BUG: failed to escape recovery parameter value \"%s\" " + "in a buffer of %d bytes", + recoveryConfString, destinationSize); + return false; + } + + destination[escapedStringLength++] = '\''; + + for (charIndex = 0; charIndex < length; charIndex++) + { + char currentChar = recoveryConfString[charIndex]; + + if (currentChar == '\'') + { + destination[escapedStringLength++] = '\''; + if (destinationSize < escapedStringLength) + { + log_error( + "BUG: failed to escape recovery parameter value \"%s\" " + "in a buffer of %d bytes, stopped at index %d", + recoveryConfString, destinationSize, charIndex); + return false; + } + } + + destination[escapedStringLength++] = currentChar; + if (destinationSize < escapedStringLength) + { + log_error("BUG: failed to escape recovery parameter value \"%s\" " + "in a buffer of %d bytes, stopped at index %d", + recoveryConfString, destinationSize, charIndex); + return false; + } + } + + destination[escapedStringLength++] = '\''; + destination[escapedStringLength] = '\0'; + + return true; +} + + +/* + * prepare_primary_conninfo prepares a connection string to the primary server. + * The connection string may be used unquoted in a command line calling either + * pg_basebackup ro pg_rewind, or may be used quoted in the primary_conninfo + * setting for PostgreSQL. + * + * Also, pg_rewind needs a database to connect to. + */ +static bool +prepare_primary_conninfo(char *primaryConnInfo, + int primaryConnInfoSize, + const char *primaryHost, + int primaryPort, + const char *replicationUsername, + const char *dbname, + const char *replicationPassword, + const char *applicationName, + SSLOptions sslOptions, + bool escape) +{ + int size = 0; + char escaped[BUFSIZE]; + + if (IS_EMPTY_STRING_BUFFER(primaryHost)) + { + log_debug("prepare_primary_conninfo: missing primary hostname"); + + bzero((void *) primaryConnInfo, primaryConnInfoSize); + + return true; + } + + PQExpBuffer buffer = createPQExpBuffer(); + + if (buffer == NULL) + { + log_error("Failed to allocate memory"); + return false; + } + + /* application_name shows up in pg_stat_replication on the primary */ + appendPQExpBuffer(buffer, "application_name=%s", applicationName); + appendPQExpBuffer(buffer, " host=%s", primaryHost); + appendPQExpBuffer(buffer, " port=%d", primaryPort); + appendPQExpBuffer(buffer, " user=%s", replicationUsername); + + if (dbname != NULL) + { + appendPQExpBuffer(buffer, " dbname=%s", dbname); + } + + if (replicationPassword != NULL && !IS_EMPTY_STRING_BUFFER(replicationPassword)) + { + appendPQExpBuffer(buffer, " password=%s", replicationPassword); + } + + appendPQExpBufferStr(buffer, " "); + if (!prepare_conninfo_sslmode(buffer, sslOptions)) + { + /* errors have already been logged */ + destroyPQExpBuffer(buffer); + return false; + } + + /* memory allocation could have failed while building string */ + if (PQExpBufferBroken(buffer)) + { + log_error("Failed to allocate memory"); + destroyPQExpBuffer(buffer); + return false; + } + + if (escape) + { + if (!escape_recovery_conf_string(escaped, BUFSIZE, buffer->data)) + { + /* errors have already been logged. */ + destroyPQExpBuffer(buffer); + return false; + } + + /* now copy the buffer into primaryConnInfo for the caller */ + size = sformat(primaryConnInfo, primaryConnInfoSize, "%s", escaped); + + if (size == -1 || size > primaryConnInfoSize) + { + log_error("BUG: the escaped primary_conninfo requires %d bytes and " + "pg_auto_failover only support up to %d bytes", + size, primaryConnInfoSize); + destroyPQExpBuffer(buffer); + return false; + } + } + else + { + strlcpy(primaryConnInfo, buffer->data, primaryConnInfoSize); + } + + destroyPQExpBuffer(buffer); + + return true; +} + + +/* + * prepare_conninfo_sslmode adds the sslmode setting to the buffer, which is + * used as a connection string. + */ +static bool +prepare_conninfo_sslmode(PQExpBuffer buffer, SSLOptions sslOptions) +{ + if (sslOptions.sslMode == SSL_MODE_UNKNOWN) + { + if (sslOptions.active) + { + /* that's a bug really */ + log_error("SSL is active in the configuration, " + "but sslmode is unknown"); + return false; + } + + return true; + } + + appendPQExpBuffer(buffer, "sslmode=%s", + pgsetup_sslmode_to_string(sslOptions.sslMode)); + + if (sslOptions.sslMode >= SSL_MODE_VERIFY_CA) + { + /* ssl revocation list might not be provided, it's ok */ + if (!IS_EMPTY_STRING_BUFFER(sslOptions.crlFile)) + { + appendPQExpBuffer(buffer, " sslrootcert=%s sslcrl=%s", + sslOptions.caFile, sslOptions.crlFile); + } + else + { + appendPQExpBuffer(buffer, " sslrootcert=%s", sslOptions.caFile); + } + } + + return true; +} + + +/* + * pgctl_identify_system connects with replication=1 to our target node and run + * the IDENTIFY_SYSTEM command to check that HBA is ready. + */ +bool +pgctl_identify_system(ReplicationSource *replicationSource) +{ + NodeAddress *primaryNode = &(replicationSource->primaryNode); + + char primaryConnInfo[MAXCONNINFO] = { 0 }; + char primaryConnInfoReplication[MAXCONNINFO] = { 0 }; + PGSQL replicationClient = { 0 }; + + if (!prepare_primary_conninfo(primaryConnInfo, + MAXCONNINFO, + primaryNode->host, + primaryNode->port, + replicationSource->userName, + NULL, /* no database */ + replicationSource->password, + replicationSource->applicationName, + replicationSource->sslOptions, + false)) /* no need for escaping */ + { + /* errors have already been logged. */ + return false; + } + + /* + * Per https://www.postgresql.org/docs/12/protocol-replication.html: + * + * To initiate streaming replication, the frontend sends the replication + * parameter in the startup message. A Boolean value of true (or on, yes, + * 1) tells the backend to go into physical replication walsender mode, + * wherein a small set of replication commands, shown below, can be issued + * instead of SQL statements. + */ + int len = sformat(primaryConnInfoReplication, MAXCONNINFO, + "%s replication=1", + primaryConnInfo); + + if (len >= MAXCONNINFO) + { + log_warn("Failed to call IDENTIFY_SYSTEM: primary_conninfo too large"); + return false; + } + + if (!pgsql_init(&replicationClient, + primaryConnInfoReplication, + PGSQL_CONN_UPSTREAM)) + { + /* errors have already been logged */ + return false; + } + + if (!pgsql_identify_system(&replicationClient, + &(replicationSource->system))) + { + /* errors have already been logged */ + return false; + } + + return true; +} + + +/* + * pg_is_running returns true if PostgreSQL is running. + */ +bool +pg_is_running(const char *pg_ctl, const char *pgdata) +{ + return pg_ctl_status(pg_ctl, pgdata, false) == 0; +} + + +/* + * pg_create_self_signed_cert creates self-signed certificates for the local + * Postgres server and places the private key in $PGDATA/server.key and the + * public certificate in $PGDATA/server.cert + * + * We simply follow Postgres documentation at: + * https://www.postgresql.org/docs/current/ssl-tcp.html#SSL-CERTIFICATE-CREATION + * + * openssl req -new -x509 -days 365 -nodes -text -out server.crt \ + * -keyout server.key -subj "/CN=dbhost.yourdomain.com" + */ +bool +pg_create_self_signed_cert(PostgresSetup *pgSetup, const char *hostname) +{ + char subject[BUFSIZE] = { 0 }; + char openssl[MAXPGPATH] = { 0 }; + + if (!search_path_first("openssl", openssl, LOG_ERROR)) + { + /* errors have already been logged */ + return false; + } + + /* ensure PGDATA has been normalized */ + if (!normalize_filename(pgSetup->pgdata, pgSetup->pgdata, MAXPGPATH)) + { + return false; + } + + int size = sformat(pgSetup->ssl.serverKey, MAXPGPATH, + "%s/server.key", pgSetup->pgdata); + + if (size == -1 || size > MAXPGPATH) + { + log_error("BUG: the ssl server key file path requires %d bytes and " + "pg_auto_failover only support up to %d bytes", + size, MAXPGPATH); + return false; + } + + size = sformat(pgSetup->ssl.serverCert, MAXPGPATH, + "%s/server.crt", pgSetup->pgdata); + + if (size == -1 || size > MAXPGPATH) + { + log_error("BUG: the ssl server key file path requires %d bytes and " + "pg_auto_failover only support up to %d bytes", + size, MAXPGPATH); + return false; + } + + size = sformat(subject, BUFSIZE, "/CN=%s", hostname); + + if (size == -1 || size > BUFSIZE) + { + log_error("BUG: the ssl subject \"/CN=%s\" requires %d bytes and" + "pg_auto_failover only support up to %d bytes", + hostname, size, BUFSIZE); + return false; + } + + log_info(" %s req -new -x509 -days 365 -nodes -text " + "-out %s -keyout %s -subj \"%s\"", + openssl, + pgSetup->ssl.serverCert, + pgSetup->ssl.serverKey, + subject); + + Program program = run_program(openssl, + "req", "-new", "-x509", "-days", "365", + "-nodes", "-text", + "-out", pgSetup->ssl.serverCert, + "-keyout", pgSetup->ssl.serverKey, + "-subj", subject, + NULL); + + if (program.returnCode != 0) + { + (void) log_program_output(program, LOG_INFO, LOG_ERROR); + log_error("openssl failed with return code: %d", program.returnCode); + free_program(&program); + return false; + } + + (void) log_program_output(program, LOG_DEBUG, LOG_DEBUG); + free_program(&program); + + /* + * Then do: chmod og-rwx server.key + */ + if (chmod(pgSetup->ssl.serverKey, S_IRUSR | S_IWUSR) != 0) + { + log_error("Failed to chmod og-rwx \"%s\": %m", pgSetup->ssl.serverKey); + return false; + } + + return true; +} diff --git a/src/bin/common/pgctl.h b/src/bin/common/pgctl.h new file mode 100644 index 000000000..e030d8217 --- /dev/null +++ b/src/bin/common/pgctl.h @@ -0,0 +1,79 @@ +/* + * src/bin/pg_autoctl/pgctl.h + * API for controling PostgreSQL, using its binary tooling (pg_ctl, + * pg_controldata, pg_basebackup and such). + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the PostgreSQL License. + * + */ + +#ifndef PGCTL_H +#define PGCTL_H + +#include +#include +#include + +#include "postgres_fe.h" +#include "utils/pidfile.h" + +#include "defaults.h" +#include "file_utils.h" +#include "pgsetup.h" +#include "pgsql.h" + +#define AUTOCTL_DEFAULTS_CONF_FILENAME "postgresql-auto-failover.conf" +#define AUTOCTL_STANDBY_CONF_FILENAME "postgresql-auto-failover-standby.conf" + +#define PG_CTL_STATUS_NOT_RUNNING 3 + +bool pg_controldata(PostgresSetup *pgSetup, bool missing_ok); +bool set_pg_ctl_from_PG_CONFIG(PostgresSetup *pgSetup); +bool set_pg_ctl_from_pg_config(PostgresSetup *pgSetup); +bool config_find_pg_ctl(PostgresSetup *pgSetup); +bool find_extension_control_file(const char *pg_ctl, const char *extName); +bool pg_ctl_version(PostgresSetup *pgSetup); +bool set_pg_ctl_from_config_bindir(PostgresSetup *pgSetup, const char *pg_config); +bool find_pg_config_from_pg_ctl(const char *pg_ctl, char *pg_config, size_t size); + +bool pg_add_auto_failover_default_settings(PostgresSetup *pgSetup, + const char *hostname, + const char *configFilePath, + GUC *settings); + +bool pg_auto_failover_default_settings_file_exists(PostgresSetup *pgSetup); + +bool pg_basebackup(const char *pgdata, + const char *pg_ctl, + ReplicationSource *replicationSource); +bool pg_rewind(const char *pgdata, + const char *pg_ctl, + ReplicationSource *replicationSource); + +bool pg_ctl_initdb(const char *pg_ctl, const char *pgdata); +bool pg_ctl_postgres(const char *pg_ctl, const char *pgdata, int pgport, + char *listen_addresses, bool listen); +bool pg_log_startup(const char *pgdata, int logLevel); +bool pg_log_recovery_setup(const char *pgdata, int logLevel); +bool pg_ctl_stop(const char *pg_ctl, const char *pgdata); +int pg_ctl_status(const char *pg_ctl, const char *pgdata, bool log_output); +bool pg_ctl_promote(const char *pg_ctl, const char *pgdata); +bool pg_ctl_reload(const char *pg_ctl, const char *pgdata); + +bool pg_setup_standby_mode(uint32_t pg_control_version, + const char *pgdata, + const char *pg_ctl, + ReplicationSource *replicationSource); + +bool pg_cleanup_standby_mode(uint32_t pg_control_version, + const char *pg_ctl, + const char *pgdata, + PGSQL *pgsql); + +bool pgctl_identify_system(ReplicationSource *replicationSource); + +bool pg_is_running(const char *pg_ctl, const char *pgdata); +bool pg_create_self_signed_cert(PostgresSetup *pgSetup, const char *hostname); + +#endif /* PGCTL_H */ diff --git a/src/bin/common/pgsetup.c b/src/bin/common/pgsetup.c new file mode 100644 index 000000000..c9e0b0ecf --- /dev/null +++ b/src/bin/common/pgsetup.c @@ -0,0 +1,2049 @@ +/* + * src/bin/pg_autoctl/pgsetup.c + * Discovers a PostgreSQL setup by calling pg_controldata and reading + * postmaster.pid file, getting clues from the process environment and from + * user given hints (options). + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the PostgreSQL License. + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "parson.h" + +#include "postgres_fe.h" +#include "pqexpbuffer.h" + +#include "defaults.h" +#include "env_utils.h" +#include "log.h" +#include "parsing.h" +#include "pgctl.h" +#include "signals.h" +#include "string_utils.h" + + +static bool get_pgpid(PostgresSetup *pgSetup, bool pgIsNotRunningIsOk); +static PostmasterStatus pmStatusFromString(const char *postmasterStatus); + + +/* + * Discover PostgreSQL environment from given clues, or a partial setup. + * + * This routines check the PATH for pg_ctl, and is ok when there's a single + * entry in found. It then uses either given PGDATA or the environment value + * and runs a pg_controldata to get system identifier and PostgreSQL version + * numbers. Then it reads PGDATA/postmaster.pid to get the pid and the port of + * the running PostgreSQL server. Then it can connects to it and see if it's in + * recovery. + */ +bool +pg_setup_init(PostgresSetup *pgSetup, + PostgresSetup *options, + bool missing_pgdata_is_ok, + bool pg_is_not_running_is_ok) +{ + int errors = 0; + + /* + * Make sure that we keep the options->nodeKind in the pgSetup. + */ + pgSetup->pgKind = options->pgKind; + + /* + * Also make sure that we keep the pg_controldata results if we have them. + */ + pgSetup->control = options->control; + + /* + * Also make sure that we keep the hbaLevel to edit. Remember that + * --skip-pg-hba is registered in the config as --auth skip. + */ + if (strcmp(options->authMethod, "skip") == 0) + { + pgSetup->hbaLevel = HBA_EDIT_SKIP; + strlcpy(pgSetup->hbaLevelStr, options->authMethod, NAMEDATALEN); + } + else + { + pgSetup->hbaLevel = options->hbaLevel; + strlcpy(pgSetup->hbaLevelStr, options->hbaLevelStr, NAMEDATALEN); + } + + /* + * Make sure that we keep the SSL options too. + */ + pgSetup->ssl.active = options->ssl.active; + pgSetup->ssl.createSelfSignedCert = options->ssl.createSelfSignedCert; + pgSetup->ssl.sslMode = options->ssl.sslMode; + strlcpy(pgSetup->ssl.sslModeStr, options->ssl.sslModeStr, SSL_MODE_STRLEN); + strlcpy(pgSetup->ssl.caFile, options->ssl.caFile, MAXPGPATH); + strlcpy(pgSetup->ssl.crlFile, options->ssl.crlFile, MAXPGPATH); + strlcpy(pgSetup->ssl.serverCert, options->ssl.serverCert, MAXPGPATH); + strlcpy(pgSetup->ssl.serverKey, options->ssl.serverKey, MAXPGPATH); + + /* Also make sure we keep the citus specific clusterName option */ + strlcpy(pgSetup->citusClusterName, options->citusClusterName, NAMEDATALEN); + + /* check or find pg_ctl, unless we already have it */ + if (IS_EMPTY_STRING_BUFFER(pgSetup->pg_ctl) || + IS_EMPTY_STRING_BUFFER(pgSetup->pg_version)) + { + if (!IS_EMPTY_STRING_BUFFER(options->pg_ctl)) + { + /* copy over pg_ctl and pg_version */ + strlcpy(pgSetup->pg_ctl, options->pg_ctl, MAXPGPATH); + strlcpy(pgSetup->pg_version, options->pg_version, + PG_VERSION_STRING_MAX); + + /* we might not have fetched the version yet */ + if (IS_EMPTY_STRING_BUFFER(pgSetup->pg_version)) + { + /* also cache the version in options */ + if (!pg_ctl_version(options)) + { + /* we already logged about it */ + return false; + } + + strlcpy(pgSetup->pg_version, + options->pg_version, + sizeof(pgSetup->pg_version)); + + log_debug("pg_setup_init: %s version %s", + pgSetup->pg_ctl, pgSetup->pg_version); + } + } + else + { + if (!config_find_pg_ctl(pgSetup)) + { + /* config_find_pg_ctl already logged errors */ + errors++; + } + } + } + + /* check or find PGDATA */ + if (options->pgdata[0] != '\0') + { + strlcpy(pgSetup->pgdata, options->pgdata, MAXPGPATH); + } + else + { + if (!get_env_pgdata(pgSetup->pgdata)) + { + log_error("Failed to set PGDATA either from the environment " + "or from --pgdata"); + errors++; + } + } + + if (!missing_pgdata_is_ok && !directory_exists(pgSetup->pgdata)) + { + log_fatal("Database directory \"%s\" not found", pgSetup->pgdata); + return false; + } + else if (!missing_pgdata_is_ok) + { + char globalControlPath[MAXPGPATH] = { 0 }; + + /* globalControlFilePath = $PGDATA/global/pg_control */ + join_path_components(globalControlPath, + pgSetup->pgdata, "global/pg_control"); + + if (!file_exists(globalControlPath)) + { + log_error("PGDATA exists but is not a Postgres directory, " + "see above for details"); + return false; + } + } + + /* get the real path of PGDATA now */ + if (directory_exists(pgSetup->pgdata)) + { + if (!normalize_filename(pgSetup->pgdata, pgSetup->pgdata, MAXPGPATH)) + { + /* errors have already been logged */ + return false; + } + } + + /* check of find username */ + if (options->username[0] != '\0') + { + strlcpy(pgSetup->username, options->username, NAMEDATALEN); + } + else + { + /* + * If a PGUSER environment variable is defined, take the value from + * there. Otherwise we attempt to connect without username. In that + * case the username will be determined based on the current user. + */ + if (!get_env_copy_with_fallback("PGUSER", pgSetup->username, NAMEDATALEN, "")) + { + /* errors have already been logged */ + return false; + } + } + + /* check or find dbname */ + if (!IS_EMPTY_STRING_BUFFER(options->dbname)) + { + strlcpy(pgSetup->dbname, options->dbname, NAMEDATALEN); + } + else + { + /* + * If a PGDATABASE environment variable is defined, take the value from + * there. Otherwise we attempt to connect without a database name, and + * the default will use the username here instead. + */ + if (!get_env_copy_with_fallback("PGDATABASE", pgSetup->dbname, NAMEDATALEN, + DEFAULT_DATABASE_NAME)) + { + /* errors have already been logged */ + return false; + } + } + + /* + * Read the postmaster.pid file to find out pid, port and unix socket + * directory of a running PostgreSQL instance. + */ + bool pgIsReady = pg_setup_is_ready(pgSetup, pg_is_not_running_is_ok); + + if (!pgIsReady && !pg_is_not_running_is_ok) + { + /* errors have already been logged */ + errors++; + } + + /* + * check or find PGHOST + * + * By order of preference, we use: + * --pghost command line option + * PGDATA/postmaster.pid + * PGHOST from the environment + */ + if (options->pghost[0] != '\0') + { + strlcpy(pgSetup->pghost, options->pghost, _POSIX_HOST_NAME_MAX); + } + else + { + /* read_pg_pidfile might already have set pghost for us */ + if (pgSetup->pghost[0] == '\0') + { + /* + * We can (at least try to) connect without host= in the connection + * string, so missing PGHOST and --pghost isn't an error. + */ + if (!get_env_copy_with_fallback("PGHOST", pgSetup->pghost, + _POSIX_HOST_NAME_MAX, "")) + { + /* errors have already been logged */ + return false; + } + } + } + + /* + * In test environment we might disable unix socket directories. In that + * case, we need to have an host to connect to, accepting to connect + * without host= in the connection string is not going to cut it. + */ + if (IS_EMPTY_STRING_BUFFER(pgSetup->pghost)) + { + if (env_found_empty("PG_REGRESS_SOCK_DIR")) + { + log_error("PG_REGRESS_SOCK_DIR is set to \"\" to disable unix " + "socket directories, now --pghost is mandatory, " + "but unset."); + errors++; + } + } + + /* check or find PGPORT + * + * By order or preference, we use: + * --pgport command line option + * PGDATA/postmaster.pid + * PGPORT from the environment + * POSTGRES_PORT from our hard coded defaults (5432, see defaults.h) + */ + if (options->pgport > 0) + { + pgSetup->pgport = options->pgport; + } + else + { + /* if we have a running cluster, just use its port */ + if (pgSetup->pidFile.pid > 0 && pgSetup->pidFile.port > 0) + { + pgSetup->pgport = pgSetup->pidFile.port; + } + else + { + /* + * no running cluster, what about using PGPORT then? + */ + pgSetup->pgport = pgsetup_get_pgport(); + } + } + + /* Set proxy port */ + if (options->proxyport > 0) + { + pgSetup->proxyport = options->proxyport; + } + + + /* + * If --listen is given, then set our listen_addresses to this value + */ + if (!IS_EMPTY_STRING_BUFFER(options->listen_addresses)) + { + strlcpy(pgSetup->listen_addresses, + options->listen_addresses, MAXPGPATH); + } + else + { + /* + * The default listen_addresses is '*', because we are dealing with a + * cluster setup and 'localhost' isn't going to cut it: the monitor and + * the coordinator nodes need to be able to connect to our local node + * using a connection string with hostname:port. + */ + strlcpy(pgSetup->listen_addresses, + POSTGRES_DEFAULT_LISTEN_ADDRESSES, MAXPGPATH); + } + + + /* + * If --auth is given, then set our authMethod to this value + * otherwise it remains empty + */ + if (!IS_EMPTY_STRING_BUFFER(options->authMethod)) + { + strlcpy(pgSetup->authMethod, + options->authMethod, NAMEDATALEN); + } + + pgSetup->settings = options->settings; + + /* + * And we always double-check with PGDATA/postmaster.pid if we have it, and + * we should have it in the normal/expected case. + */ + if (pgIsReady && + pgSetup->pidFile.pid > 0 && + pgSetup->pgport != pgSetup->pidFile.port) + { + log_error("Given --pgport %d doesn't match PostgreSQL " + "port %d from \"%s/postmaster.pid\"", + pgSetup->pgport, pgSetup->pidFile.port, pgSetup->pgdata); + errors++; + } + + /* + * When we have a PGDATA and Postgres is not running, we need to grab more + * information about the local installation: pg_controldata can give us the + * pg-_control_version, catalog_version_no, and system_identifier. + */ + if (errors == 0) + { + /* + * Only run pg_controldata when Postgres is not running, otherwise we + * get the same information later from an SQL query, see + * pgsql_get_postgres_metadata. + */ + if (!pg_setup_is_running(pgSetup) && + pgSetup->control.pg_control_version == 0) + { + pg_controldata(pgSetup, missing_pgdata_is_ok); + + if (pgSetup->control.pg_control_version == 0) + { + /* we already logged about it */ + if (!missing_pgdata_is_ok) + { + errors++; + } + } + else + { + log_debug("Found PostgreSQL system %" PRIu64 " at \"%s\", " + "version %u, catalog version %u", + pgSetup->control.system_identifier, + pgSetup->pgdata, + pgSetup->control.pg_control_version, + pgSetup->control.catalog_version_no); + } + } + } + + /* + * Sometimes `pg_ctl start` returns with success and Postgres is still in + * crash recovery replaying WAL files, in the "starting" state rather than + * the "ready" state. + * + * In that case, we wait until Postgres is ready for connections. The whole + * pg_autoctl code is expecting to be able to connect to Postgres, so + * there's no point in returning now and having the next connection attempt + * fail with something like the following: + * + * ERROR Connection to database failed: FATAL: the database system is + * starting up + */ + if (pgSetup->pidFile.port > 0 && + pgSetup->pgport == pgSetup->pidFile.port) + { + if (!pgIsReady) + { + if (!pg_is_not_running_is_ok) + { + log_error("Failed to read Postgres pidfile, " + "see above for details"); + return false; + } + } + } + + if (errors > 0) + { + log_fatal("Failed to discover PostgreSQL setup, " + "please fix previous errors."); + return false; + } + + return true; +} + + +/* + * Read the first line of the PGDATA/postmaster.pid file to get Postgres PID. + */ +static bool +get_pgpid(PostgresSetup *pgSetup, bool pgIsNotRunningIsOk) +{ + char *contents = NULL; + long fileSize = 0; + char pidfile[MAXPGPATH]; + char *lines[1]; + int pid = -1; + + /* when !pgIsNotRunningIsOk then log_error(), otherwise log_debug() */ + int logLevel = pgIsNotRunningIsOk ? LOG_TRACE : LOG_ERROR; + + join_path_components(pidfile, pgSetup->pgdata, "postmaster.pid"); + + if (!read_file_if_exists(pidfile, &contents, &fileSize)) + { + log_level(logLevel, "Failed to open file \"%s\": %m", pidfile); + + if (!pgIsNotRunningIsOk) + { + log_info("Is PostgreSQL at \"%s\" up and running?", pgSetup->pgdata); + } + return false; + } + + if (fileSize == 0) + { + /* yeah, that happens (race condition, kind of) */ + log_debug("The PID file \"%s\" is empty", pidfile); + free(contents); + return false; + } + else if (splitLines(contents, lines, 1) != 1 || + !stringToInt(lines[0], &pid)) + { + log_warn("Invalid data in PID file \"%s\"", pidfile); + free(contents); + return false; + } + + free(contents); + contents = NULL; + + /* postmaster PID (or negative of a standalone backend's PID) */ + if (pid < 0) + { + int standalonePid = -1 * pid; + + if (kill(standalonePid, 0) == 0) + { + pgSetup->pidFile.pid = pid; + return true; + } + log_debug("Read a stale standalone pid in \"postmaster.pid\": %d", pid); + return false; + } + else if (pid > 0 && pid <= INT_MAX) + { + if (kill(pid, 0) == 0) + { + pgSetup->pidFile.pid = pid; + return true; + } + else + { + logLevel = pgIsNotRunningIsOk ? LOG_DEBUG : LOG_WARN; + + log_level(logLevel, + "Read a stale pid in \"postmaster.pid\": %d", pid); + + return false; + } + } + else + { + /* that's more like a bug, really */ + log_error("Invalid PID \"%d\" read in \"postmaster.pid\"", pid); + return false; + } +} + + +/* + * Read the PGDATA/postmaster.pid file to get the port number of the running + * server we're asked to keep highly available. + */ +bool +read_pg_pidfile(PostgresSetup *pgSetup, bool pgIsNotRunningIsOk, int maxRetries) +{ + FILE *fp; + int lineno; + char line[BUFSIZE]; + char pidfile[MAXPGPATH]; + + join_path_components(pidfile, pgSetup->pgdata, "postmaster.pid"); + + if ((fp = fopen_read_only(pidfile)) == NULL) + { + /* + * Maybe we're attempting to read the file during Postgres start-up + * phase and we just got where the file is replaced, when going from + * standalone backend to full service. + */ + if (maxRetries > 0) + { + log_trace("read_pg_pidfile: \"%s\" does not exist [%d]", + pidfile, maxRetries); + pg_usleep(250 * 1000); /* wait for 250ms and try again */ + return read_pg_pidfile(pgSetup, pgIsNotRunningIsOk, maxRetries - 1); + } + + if (!pgIsNotRunningIsOk) + { + log_error("Failed to open file \"%s\": %m", pidfile); + log_info("Is PostgreSQL at \"%s\" up and running?", pgSetup->pgdata); + } + return false; + } + + for (lineno = 1; lineno <= LOCK_FILE_LINE_PM_STATUS; lineno++) + { + if (fgets(line, sizeof(line), fp) == NULL) + { + /* later lines are added during start-up, will appear later */ + if (lineno > LOCK_FILE_LINE_PORT) + { + /* that's retry-able */ + fclose(fp); + + if (maxRetries == 0) + { + /* partial read is ok, pgSetup keeps track */ + return true; + } + + pg_usleep(250 * 1000); /* sleep for 250ms */ + log_trace("read_pg_pidfile: fgets is NULL for lineno %d, retry %d", + lineno, maxRetries); + return read_pg_pidfile(pgSetup, + pgIsNotRunningIsOk, + maxRetries - 1); + } + else + { + /* don't use %m to print errno, errno is not set by fgets */ + log_error("Failed to read line %d from file \"%s\"", + lineno, pidfile); + fclose(fp); + return false; + } + } + + int lineLength = strlen(line); + + /* chomp the ending Newline (\n) */ + if (lineLength > 0) + { + line[lineLength - 1] = '\0'; + lineLength = strlen(line); + } + + if (lineno == LOCK_FILE_LINE_PID) + { + int pid = 0; + if (!stringToInt(line, &pid)) + { + log_error("Postgres pidfile does not contain a valid pid %s", + line); + + return false; + } + + /* a standalone backend pid is negative, we signal the actual pid */ + pgSetup->pidFile.pid = abs(pid); + + if (kill(pgSetup->pidFile.pid, 0) != 0) + { + log_error("Postgres pidfile contains pid %d, " + "which is not running", pgSetup->pidFile.pid); + + /* well then reset the PID to our unknown value */ + pgSetup->pidFile.pid = 0; + + return false; + } + + if (pid < 0) + { + /* standalone backend during the start-up process */ + break; + } + } + + if (lineno == LOCK_FILE_LINE_PORT) + { + if (!stringToUShort(line, &pgSetup->pidFile.port)) + { + log_error("Postgres pidfile does not contain a valid port %s", + line); + + return false; + } + } + + if (lineno == LOCK_FILE_LINE_SOCKET_DIR) + { + if (lineLength > 0) + { + int n = strlcpy(pgSetup->pghost, line, _POSIX_HOST_NAME_MAX); + + if (n >= _POSIX_HOST_NAME_MAX) + { + log_error("Failed to read unix socket directory \"%s\" " + "from file \"%s\": the directory name is %d " + "characters long, " + "and pg_autoctl only accepts up to %d characters", + line, pidfile, n, _POSIX_HOST_NAME_MAX - 1); + return false; + } + } + } + + if (lineno == LOCK_FILE_LINE_PM_STATUS) + { + if (lineLength > 0) + { + pgSetup->pm_status = pmStatusFromString(line); + } + } + } + fclose(fp); + + log_trace("read_pg_pidfile: pid %d, port %d, host %s, status \"%s\"", + pgSetup->pidFile.pid, + pgSetup->pidFile.port, + pgSetup->pghost, + pmStatusToString(pgSetup->pm_status)); + + return true; +} + + +/* + * fprintf_pg_setup prints to given STREAM the current setting found in + * pgSetup. + */ +void +fprintf_pg_setup(FILE *stream, PostgresSetup *pgSetup) +{ + int pgversion = 0; + + (void) parse_pg_version_string(pgSetup->pg_version, &pgversion); + + fformat(stream, "pgdata: %s\n", pgSetup->pgdata); + fformat(stream, "pg_ctl: %s\n", pgSetup->pg_ctl); + + fformat(stream, "pg_version: \"%s\" (%d)\n", + pgSetup->pg_version, pgversion); + + fformat(stream, "pghost: %s\n", pgSetup->pghost); + fformat(stream, "pgport: %d\n", pgSetup->pgport); + fformat(stream, "proxyport: %d\n", pgSetup->proxyport); + fformat(stream, "pid: %d\n", pgSetup->pidFile.pid); + fformat(stream, "is in recovery: %s\n", + pgSetup->is_in_recovery ? "yes" : "no"); + fformat(stream, "Control cluster state: %s\n", + dbstateToString(pgSetup->control.state)); + fformat(stream, "Control Version: %u\n", + pgSetup->control.pg_control_version); + fformat(stream, "Catalog Version: %u\n", + pgSetup->control.catalog_version_no); + fformat(stream, "System Identifier: %" PRIu64 "\n", + pgSetup->control.system_identifier); + fformat(stream, "Latest checkpoint LSN: %s\n", + pgSetup->control.latestCheckpointLSN); + fformat(stream, "Postmaster status: %s\n", + pmStatusToString(pgSetup->pm_status)); + fflush(stream); +} + + +/* + * pg_setup_as_json copies in the given pre-allocated string the json + * representation of the pgSetup. + */ +bool +pg_setup_as_json(PostgresSetup *pgSetup, JSON_Value *js) +{ + JSON_Object *jsobj = json_value_get_object(js); + char system_identifier[BUFSIZE]; + + json_object_set_string(jsobj, "pgdata", pgSetup->pgdata); + json_object_set_string(jsobj, "pg_ctl", pgSetup->pg_ctl); + json_object_set_string(jsobj, "version", pgSetup->pg_version); + json_object_set_string(jsobj, "host", pgSetup->pghost); + json_object_set_number(jsobj, "port", (double) pgSetup->pgport); + json_object_set_number(jsobj, "proxyport", (double) pgSetup->proxyport); + json_object_set_number(jsobj, "pid", (double) pgSetup->pidFile.pid); + json_object_set_boolean(jsobj, "in_recovery", pgSetup->is_in_recovery); + + json_object_dotset_number(jsobj, + "control.version", + (double) pgSetup->control.pg_control_version); + + json_object_dotset_number(jsobj, + "control.catalog_version", + (double) pgSetup->control.catalog_version_no); + + sformat(system_identifier, BUFSIZE, "%" PRIu64, + pgSetup->control.system_identifier); + json_object_dotset_string(jsobj, + "control.system_identifier", + system_identifier); + + json_object_dotset_string(jsobj, + "postmaster.status", + pmStatusToString(pgSetup->pm_status)); + return true; +} + + +/* + * pg_setup_get_local_connection_string build a connecting string to connect + * to the local postgres server and writes it to connectionString, which should + * be at least MAXCONNINFO in size. + */ +bool +pg_setup_get_local_connection_string(PostgresSetup *pgSetup, + char *connectionString) +{ + char pg_regress_sock_dir[MAXPGPATH] = { 0 }; + bool pg_regress_sock_dir_exists = env_exists("PG_REGRESS_SOCK_DIR"); + PQExpBuffer connStringBuffer = createPQExpBuffer(); + + if (connStringBuffer == NULL) + { + log_error("Failed to allocate memory"); + return false; + } + + appendPQExpBuffer(connStringBuffer, "port=%d dbname=%s", + pgSetup->pgport, pgSetup->dbname); + + if (pg_regress_sock_dir_exists && + !get_env_copy("PG_REGRESS_SOCK_DIR", pg_regress_sock_dir, MAXPGPATH)) + { + /* errors have already been logged */ + destroyPQExpBuffer(connStringBuffer); + return false; + } + + /* + * When PG_REGRESS_SOCK_DIR is set and empty, we force the connection + * string to use "localhost" (TCP/IP hostname for IP 127.0.0.1 or ::1, + * usually), even when the configuration setup is using a unix directory + * setting. + */ + if (env_found_empty("PG_REGRESS_SOCK_DIR") && + (IS_EMPTY_STRING_BUFFER(pgSetup->pghost) || + pgSetup->pghost[0] == '/')) + { + appendPQExpBufferStr(connStringBuffer, " host=localhost"); + } + else if (!IS_EMPTY_STRING_BUFFER(pgSetup->pghost)) + { + if (pg_regress_sock_dir_exists && strlen(pg_regress_sock_dir) > 0 && + strcmp(pgSetup->pghost, pg_regress_sock_dir) != 0) + { + /* + * It might turn out ok (stray environment), but in case of + * connection error, this warning should be useful to debug the + * situation. + */ + log_warn("PG_REGRESS_SOCK_DIR is set to \"%s\", " + "and our setup is using \"%s\"", + pg_regress_sock_dir, + pgSetup->pghost); + } + appendPQExpBuffer(connStringBuffer, " host=%s", pgSetup->pghost); + } + + if (!IS_EMPTY_STRING_BUFFER(pgSetup->username)) + { + appendPQExpBuffer(connStringBuffer, " user=%s", pgSetup->username); + } + + if (PQExpBufferBroken(connStringBuffer)) + { + log_error("Failed to allocate memory"); + destroyPQExpBuffer(connStringBuffer); + return false; + } + + if (strlcpy(connectionString, + connStringBuffer->data, MAXCONNINFO) >= MAXCONNINFO) + { + log_error("Failed to copy connection string \"%s\" which is %lu bytes " + "long, pg_autoctl only supports connection strings up to " + " %lu bytes", + connStringBuffer->data, + (unsigned long) connStringBuffer->len, + (unsigned long) MAXCONNINFO); + destroyPQExpBuffer(connStringBuffer); + return false; + } + + destroyPQExpBuffer(connStringBuffer); + return true; +} + + +/* + * pg_setup_pgdata_exists returns true when PGDATA exists, hosts a + * global/pg_control file (so that it looks like a Postgres cluster) and when + * the pg_controldata probe was successful. + */ +bool +pg_setup_pgdata_exists(PostgresSetup *pgSetup) +{ + char globalControlPath[MAXPGPATH] = { 0 }; + + /* make sure our cached value in pgSetup still makes sense */ + if (!directory_exists(pgSetup->pgdata)) + { + return false; + } + + /* globalControlFilePath = $PGDATA/global/pg_control */ + join_path_components(globalControlPath, pgSetup->pgdata, "global/pg_control"); + + if (!file_exists(globalControlPath)) + { + return false; + } + + /* + * Now that we know that PGDATA exists, let's grab the system identifier if + * we don't have it already. + */ + if (pgSetup->control.system_identifier == 0) + { + bool missingPgdataIsOk = false; + + /* errors are logged from within pg_controldata */ + (void) pg_controldata(pgSetup, missingPgdataIsOk); + + return pgSetup->control.system_identifier != 0; + } + + return true; +} + + +/* + * pg_setup_pgdata_exists returns true when the pg_controldata probe was + * susccessful. + */ +bool +pg_setup_is_running(PostgresSetup *pgSetup) +{ + bool pgIsNotRunningIsOk = true; + + return pgSetup->pidFile.pid != 0 + + /* if we don't have the PID yet, try reading it now */ + || (get_pgpid(pgSetup, pgIsNotRunningIsOk) && + pgSetup->pidFile.pid > 0); +} + + +/* + * pg_setup_is_ready returns true when the postmaster.pid file has a "ready" + * status in it, which we parse in pgSetup->pm_status. + */ +bool +pg_setup_is_ready(PostgresSetup *pgSetup, bool pgIsNotRunningIsOk) +{ + char globalControlPath[MAXPGPATH] = { 0 }; + + /* globalControlFilePath = $PGDATA/global/pg_control */ + join_path_components(globalControlPath, pgSetup->pgdata, "global/pg_control"); + + if (!file_exists(globalControlPath)) + { + return false; + } + + /* + * Invalidate in-memory Postmaster status cache. + * + * This makes sure we enter the main loop and attempt to read the + * postmaster.pid file at least once: if Postgres was stopped, then the + * file that we've read previously might not exists anymore. + */ + pgSetup->pm_status = POSTMASTER_STATUS_UNKNOWN; + + /* + * Sometimes `pg_ctl start` returns with success and Postgres is still + * in crash recovery replaying WAL files, in the "starting" state + * rather than the "ready" state. + * + * In that case, we wait until Postgres is ready for connections. The + * whole pg_autoctl code is expecting to be able to connect to + * Postgres, so there's no point in returning now and having the next + * connection attempt fail with something like the following: + * + * ERROR Connection to database failed: FATAL: the database system is + * starting up + */ + while (pgSetup->pm_status != POSTMASTER_STATUS_READY) + { + int maxRetries = 5; + + if (!get_pgpid(pgSetup, pgIsNotRunningIsOk)) + { + /* + * We failed to read the Postgres pid file, and infinite + * looping might not help here anymore. Better give control + * back to the launching process (might be init scripts, + * systemd or the like) so that they may log a transient + * failure and try again. + */ + if (!pgIsNotRunningIsOk) + { + log_error("Failed to get Postgres pid, " + "see above for details"); + } + + /* + * we failed to get Postgres pid from the first line of its pid + * file, so we consider that Postgres is not running, thus not + * ready. + */ + return false; + } + + /* + * When starting up we might read the postmaster.pid file too + * early, when Postgres is still in its "standalone backend" phase. + * Let's give it 250ms before trying again then. + */ + if (pgSetup->pidFile.pid < 0) + { + pg_usleep(250 * 1000); + continue; + } + + /* + * Here, we know that Postgres is running, and we even have its + * PID. Time to try and read the rest of the PID file. This might + * fail when the file isn't complete yet, in which case we're going + * to retry. + */ + if (!read_pg_pidfile(pgSetup, pgIsNotRunningIsOk, maxRetries)) + { + log_warn("Failed to read Postgres \"postmaster.pid\" file"); + return false; + } + + /* avoid an extra wait if that's possible */ + if (pgSetup->pm_status == POSTMASTER_STATUS_READY) + { + break; + } + + log_debug("postmaster status is \"%s\", retrying in %ds.", + pmStatusToString(pgSetup->pm_status), + PG_AUTOCTL_KEEPER_RETRY_TIME_MS); + + pg_usleep(PG_AUTOCTL_KEEPER_RETRY_TIME_MS * 1000); + } + + if (pgSetup->pm_status != POSTMASTER_STATUS_UNKNOWN) + { + log_trace("pg_setup_is_ready: %s", pmStatusToString(pgSetup->pm_status)); + } + + return pgSetup->pm_status == POSTMASTER_STATUS_READY; +} + + +/* + * pg_setup_wait_until_is_ready loops over pg_setup_is_running() and returns + * when Postgres is ready. The loop tries every 100ms up to the given timeout, + * given in seconds. + */ +bool +pg_setup_wait_until_is_ready(PostgresSetup *pgSetup, int timeout, int logLevel) +{ + uint64_t startTime = time(NULL); + int attempts = 0; + + pid_t previousPostgresPid = pgSetup->pidFile.pid; + bool pgIsRunning = false; + bool pgIsReady = false; + + bool missingPgdataIsOk = false; + bool postgresNotRunningIsOk = true; + + log_trace("pg_setup_wait_until_is_ready"); + + for (attempts = 1; !pgIsRunning; attempts++) + { + uint64_t now = time(NULL); + + /* sleep 100 ms in between postmaster.pid probes */ + pg_usleep(100 * 1000); + + pgIsRunning = get_pgpid(pgSetup, postgresNotRunningIsOk) && + pgSetup->pidFile.pid > 0; + + /* let's not be THAT verbose about it */ + if ((attempts - 1) % 10 == 0) + { + log_debug("pg_setup_wait_until_is_ready(): postgres %s, " + "pid %d (was %d), after %ds and %d attempt(s)", + pgIsRunning ? "is running" : "is not running", + pgSetup->pidFile.pid, + previousPostgresPid, + (int) (now - startTime), + attempts); + } + + /* we're done if we reach the timeout */ + if ((now - startTime) >= timeout) + { + break; + } + } + + /* + * Now update our pgSetup from the running database, including versions and + * all we can discover. + */ + if (pgIsRunning && previousPostgresPid != pgSetup->pidFile.pid) + { + /* + * Update our pgSetup view of Postgres once we have made sure it's + * running. + */ + PostgresSetup newPgSetup = { 0 }; + + if (!pg_setup_init(&newPgSetup, + pgSetup, + missingPgdataIsOk, + postgresNotRunningIsOk)) + { + /* errors have already been logged */ + log_error("pg_setup_wait_until_is_ready: pg_setup_init is false"); + return false; + } + + *pgSetup = newPgSetup; + + /* avoid an extra pg_setup_is_ready call if we're all good already */ + pgIsReady = pgSetup->pm_status == POSTMASTER_STATUS_READY; + } + + /* + * Ok so we have a postmaster.pid file with a pid > 0 (not a standalone + * backend, the service has started). Postgres might still be "starting" + * rather than "ready" though, so let's continue our attempts and make sure + * that Postgres is ready. + */ + for (; !pgIsReady; attempts++) + { + uint64_t now = time(NULL); + + pgIsReady = pg_setup_is_ready(pgSetup, postgresNotRunningIsOk); + + /* let's not be THAT verbose about it */ + if ((attempts - 1) % 10 == 0) + { + log_debug("pg_setup_wait_until_is_ready(): pgstatus is %s, " + "pid %d (was %d), after %ds and %d attempt(s)", + pmStatusToString(pgSetup->pm_status), + pgSetup->pidFile.pid, + previousPostgresPid, + (int) (now - startTime), + attempts); + } + + /* we're done if we reach the timeout */ + if ((now - startTime) >= timeout) + { + break; + } + + /* sleep 100 ms in between postmaster.pid probes */ + pg_usleep(100 * 1000); + } + + if (!pgIsReady) + { + /* offer more diagnostic information to the user */ + postgresNotRunningIsOk = false; + pgIsReady = pg_setup_is_ready(pgSetup, postgresNotRunningIsOk); + + log_trace("pg_setup_wait_until_is_ready returns %s [%s]", + pgIsReady ? "true" : "false", + pmStatusToString(pgSetup->pm_status)); + + return pgIsReady; + } + + /* here we know that pgIsReady is true */ + log_level(logLevel, + "Postgres is now serving PGDATA \"%s\" on port %d with pid %d", + pgSetup->pgdata, pgSetup->pgport, pgSetup->pidFile.pid); + return true; +} + + +/* + * pg_setup_wait_until_is_stopped loops over pg_ctl_status() and returns when + * Postgres is stopped. The loop tries every 100ms up to the given timeout, + * given in seconds. + */ +bool +pg_setup_wait_until_is_stopped(PostgresSetup *pgSetup, int timeout, int logLevel) +{ + uint64_t startTime = time(NULL); + int attempts = 0; + int status = -1; + + pid_t previousPostgresPid = pgSetup->pidFile.pid; + + bool missingPgdataIsOk = false; + bool postgresNotRunningIsOk = true; + + for (attempts = 1; status != PG_CTL_STATUS_NOT_RUNNING; attempts++) + { + uint64_t now = time(NULL); + + /* + * If we don't have a postmaster.pid consider that Postgres is not + * running. + */ + if (!get_pgpid(pgSetup, postgresNotRunningIsOk)) + { + return true; + } + + /* we don't log the output for pg_ctl_status here */ + status = pg_ctl_status(pgSetup->pg_ctl, pgSetup->pgdata, false); + + log_trace("keeper_update_postgres_expected_status(): " + "pg_ctl status is %d (we expect %d: not running), " + "after %ds and %d attempt(s)", + status, + PG_CTL_STATUS_NOT_RUNNING, + (int) (now - startTime), + attempts); + + if (status == PG_CTL_STATUS_NOT_RUNNING) + { + return true; + } + + /* we're done if we reach the timeout */ + if ((now - startTime) >= timeout) + { + break; + } + + /* wait for 100 ms and try again */ + pg_usleep(100 * 1000); + } + + /* update settings from running database */ + if (previousPostgresPid != pgSetup->pidFile.pid) + { + /* + * Update our pgSetup view of Postgres once we have made sure it's + * running. + */ + PostgresSetup newPgSetup = { 0 }; + + if (!pg_setup_init(&newPgSetup, + pgSetup, + missingPgdataIsOk, + postgresNotRunningIsOk)) + { + /* errors have already been logged */ + return false; + } + + *pgSetup = newPgSetup; + + log_level(logLevel, + "Postgres is now stopped for PGDATA \"%s\"", + pgSetup->pgdata); + } + + return status == PG_CTL_STATUS_NOT_RUNNING; +} + + +/* + * pg_setup_role returns an enum value representing which role the local + * PostgreSQL instance currently has. We detect primary and secondary when + * Postgres is running, and either recovery or unknown when Postgres is not + * running. + */ +PostgresRole +pg_setup_role(PostgresSetup *pgSetup) +{ + char *pgdata = pgSetup->pgdata; + + if (pg_setup_is_running(pgSetup)) + { + /* + * Here we have either a recovery or a standby node. We don't know for + * sure with just that piece of information. + * + * If we are using Postgres 12+ and there's a standby.signal file in + * PGDATA, that's a strong hint that we can't have in previous version + * short of parsing recovery.conf. + * + * Remember that in versions before Postgres 12 the standby_mode was + * not exposed as a GUC so we can't inquire about that either. We would + * have to parse the recovery.conf file for getting the standby mode. + * + * It's easier to just return POSTGRES_ROLE_RECOVERY in that case, and + * let the caller figure out that this might be POSTGRES_ROLE_STANDBY. + * At the moment the callers don't need that level of detail anyway. + */ + if (pgSetup->is_in_recovery) + { + char recoverySignalPath[MAXPGPATH] = { 0 }; + + join_path_components(recoverySignalPath, pgdata, "standby.signal"); + + if (file_exists(recoverySignalPath)) + { + return POSTGRES_ROLE_STANDBY; + } + else + { + /* We are in recovery, we don't know if we are a standby */ + return POSTGRES_ROLE_RECOVERY; + } + } + + /* + * Here it's running and SELECT pg_is_in_recovery() is false, so we + * know we are talking about a primary server. + */ + else + { + return POSTGRES_ROLE_PRIMARY; + } + } + else + { + /* + * PostgreSQL is not running, we don't know yet... what we know is that + * to be a standby the file $PDGATA/recovery.conf needs to be setup (up + * to version 11 included), or the file $PGDATA/standby.signal needs to + * exists (starting with version 12). A recovery.signal file starting + * in Postgres 12 also indicates that we're not a primary server. + * + * There's no way that a Postgres instance is going to be a recovery or + * standby node without one of those files existing: + */ + char standbyFilesArray[][MAXPGPATH] = { + "recovery.conf", + "recovery.signal", + "standby.signal" + }; + PostgresRole standbyRoleArray[] = { + /* default to recovery, might be a standby */ + POSTGRES_ROLE_RECOVERY, /* recovery.conf */ + POSTGRES_ROLE_RECOVERY, /* recovery.signal */ + POSTGRES_ROLE_STANDBY /* standby.signal */ + }; + int pos = 0, count = 3; + + for (pos = 0; pos < count; pos++) + { + char filePath[MAXPGPATH] = { 0 }; + + join_path_components(filePath, pgdata, standbyFilesArray[pos]); + + if (file_exists(filePath)) + { + return standbyRoleArray[pos]; + } + } + + /* + * Postgres is not running, and there's no file around in PGDATA that + * allows us to have a strong opinion on whether this instance is a + * primary or a standby. It might be either. + */ + return POSTGRES_ROLE_UNKNOWN; + } + + return POSTGRES_ROLE_UNKNOWN; +} + + +/* + * pg_setup_get_username returns pgSetup->username when it exists, otherwise it + * looksup the username in passwd. Lastly it fallsback to the USER environment + * variable. When nothing works it returns DEFAULT_USERNAME PGUSER is only used + * when creating our configuration for the first time. + */ +char * +pg_setup_get_username(PostgresSetup *pgSetup) +{ + char userEnv[NAMEDATALEN] = { 0 }; + + /* use a configured username if provided */ + if (!IS_EMPTY_STRING_BUFFER(pgSetup->username)) + { + return pgSetup->username; + } + + log_trace("username not configured"); + + /* use the passwd file to find the username, same as whoami */ + uid_t uid = geteuid(); + struct passwd *pw = getpwuid(uid); + if (pw) + { + log_trace("username found in passwd: %s", pw->pw_name); + + strlcpy(pgSetup->username, pw->pw_name, sizeof(pgSetup->username)); + return pgSetup->username; + } + + + /* fallback on USER from env if the user cannot be found in passwd */ + if (get_env_copy("USER", userEnv, NAMEDATALEN)) + { + log_trace("username found in USER environment variable: %s", userEnv); + + strlcpy(pgSetup->username, userEnv, sizeof(pgSetup->username)); + return pgSetup->username; + } + + log_trace("username fallback to default: %s", DEFAULT_USERNAME); + strlcpy(pgSetup->username, DEFAULT_USERNAME, sizeof(pgSetup->username)); + + return pgSetup->username; +} + + +/* + * pg_setup_get_auth_method returns pgSetup->authMethod when it exists, + * otherwise it returns DEFAULT_AUTH_METHOD + */ +char * +pg_setup_get_auth_method(PostgresSetup *pgSetup) +{ + if (!IS_EMPTY_STRING_BUFFER(pgSetup->authMethod)) + { + return pgSetup->authMethod; + } + + log_trace("auth method not configured, falling back to default value : %s", + DEFAULT_AUTH_METHOD); + + return DEFAULT_AUTH_METHOD; +} + + +/* + * pg_setup_skip_hba_edits returns true when the user had setup pg_autoctl to + * skip editing HBA entries. + */ +bool +pg_setup_skip_hba_edits(PostgresSetup *pgSetup) +{ + return pgSetup->hbaLevel == HBA_EDIT_SKIP; +} + + +/* + * pg_setup_set_absolute_pgdata uses realpath(3) to make sure that we re using + * the absolute real pathname for PGDATA in our setup, so that services will + * work correctly after keeper/monitor init, even when initializing in a + * relative path and starting the service from elsewhere. This function returns + * true if the pgdata path has been updated in the setup. + */ +bool +pg_setup_set_absolute_pgdata(PostgresSetup *pgSetup) +{ + return normalize_filename(pgSetup->pgdata, pgSetup->pgdata, MAXPGPATH); +} + + +/* + * nodeKindFromString returns a PgInstanceKind from a string. + */ +PgInstanceKind +nodeKindFromString(const char *nodeKind) +{ + PgInstanceKind kindArray[] = { + NODE_KIND_UNKNOWN, + NODE_KIND_UNKNOWN, + NODE_KIND_STANDALONE, + NODE_KIND_CITUS_COORDINATOR, + NODE_KIND_CITUS_WORKER + }; + char *kindList[] = { + "", "unknown", "standalone", "coordinator", "worker", NULL + }; + + for (int listIndex = 0; kindList[listIndex] != NULL; listIndex++) + { + char *candidate = kindList[listIndex]; + + if (strcmp(nodeKind, candidate) == 0) + { + PgInstanceKind pgKind = kindArray[listIndex]; + log_trace("nodeKindFromString: \"%s\" ➜ %d", nodeKind, pgKind); + return pgKind; + } + } + + log_fatal("Failed to parse nodeKind \"%s\"", nodeKind); + + /* never happens, make compiler happy */ + return NODE_KIND_UNKNOWN; +} + + +/* + * nodeKindToString returns a textual representatin of given PgInstanceKind. + * This must be kept in sync with src/monitor/formation_metadata.c function + * FormationKindFromNodeKindString. + */ +char * +nodeKindToString(PgInstanceKind kind) +{ + switch (kind) + { + case NODE_KIND_STANDALONE: + { + return "standalone"; + } + + case NODE_KIND_CITUS_COORDINATOR: + { + return "coordinator"; + } + + case NODE_KIND_CITUS_WORKER: + { + return "worker"; + } + + default: + { + log_fatal("nodeKindToString: unknown node kind %d", kind); + return NULL; + } + } + + /* can't happen, keep compiler happy */ + return NULL; +} + + +/* + * pmStatusFromString parses the Postgres postmaster.pid PM_STATUS line into + * our own enum to represent the value. + */ +static PostmasterStatus +pmStatusFromString(const char *postmasterStatus) +{ + if (strcmp(postmasterStatus, PM_STATUS_STARTING) == 0) + { + return POSTMASTER_STATUS_STARTING; + } + else if (strcmp(postmasterStatus, PM_STATUS_STOPPING) == 0) + { + return POSTMASTER_STATUS_STOPPING; + } + else if (strcmp(postmasterStatus, PM_STATUS_READY) == 0) + { + return POSTMASTER_STATUS_READY; + } + else if (strcmp(postmasterStatus, PM_STATUS_STANDBY) == 0) + { + return POSTMASTER_STATUS_STANDBY; + } + + log_warn("Failed to read Postmaster status: \"%s\"", postmasterStatus); + return POSTMASTER_STATUS_UNKNOWN; +} + + +/* + * pmStatusToString returns a textual representation of given Postmaster status + * given as a PmStatus enum. + * + * We're not using the PM_STATUS_READY etc constants here because those are + * blank-padded to always be the same length, and then the warning messages + * including "ready " look buggy in a way. + */ +char * +pmStatusToString(PostmasterStatus pm_status) +{ + switch (pm_status) + { + case POSTMASTER_STATUS_UNKNOWN: + { + return "unknown"; + } + + case POSTMASTER_STATUS_STARTING: + { + return "starting"; + } + + case POSTMASTER_STATUS_STOPPING: + { + return "stopping"; + } + + case POSTMASTER_STATUS_READY: + { + return "ready"; + } + + case POSTMASTER_STATUS_STANDBY: + { + return "standby"; + } + } + + /* keep compiler happy */ + return "unknown"; +} + + +/* + * pgsetup_get_pgport returns the port to use either from the PGPORT + * environment variable, or from our default hard-coded value of 5432. + */ +int +pgsetup_get_pgport() +{ + char pgport_env[NAMEDATALEN]; + int pgport = 0; + + if (env_exists("PGPORT") && get_env_copy("PGPORT", pgport_env, NAMEDATALEN)) + { + if (stringToInt(pgport_env, &pgport) && pgport > 0) + { + return pgport; + } + else + { + log_warn("Failed to parse PGPORT value \"%s\", using %d", + pgport_env, POSTGRES_PORT); + return POSTGRES_PORT; + } + } + else + { + /* no PGPORT */ + return POSTGRES_PORT; + } +} + + +/* + * pgsetup_validate_ssl_settings returns true if our SSL settings are following + * one of the three following cases: + * + * - --no-ssl: ssl is not activated and no file has been provided + * - --ssl-self-signed: ssl is activated and no file has been provided + * - --ssl-*-files: ssl is activated and all the files have been provided + * + * Otherwise it logs an error message and return false. + */ +bool +pgsetup_validate_ssl_settings(PostgresSetup *pgSetup) +{ + SSLOptions *ssl = &(pgSetup->ssl); + + log_trace("pgsetup_validate_ssl_settings"); + + /* + * When using the full SSL options, we validate that the files exists where + * given and set the default sslmode to verify-full. + * + * --ssl-ca-file + * --ssl-crl-file + * --server-cert + * --server-key + */ + if (ssl->active && !ssl->createSelfSignedCert) + { + /* + * When passing files in manually for SSL we need at least cert and a + * key + */ + if (IS_EMPTY_STRING_BUFFER(ssl->serverCert) || + IS_EMPTY_STRING_BUFFER(ssl->serverKey)) + { + log_error("Failed to setup SSL with user-provided certificates: " + "options --server-cert and --server-key are required."); + return false; + } + + /* check that the given files exist */ + if (!file_exists(ssl->serverCert)) + { + log_error("--server-cert file does not exist at \"%s\"", + ssl->serverCert); + return false; + } + + if (!file_exists(ssl->serverKey)) + { + log_error("--server-key file does not exist at \"%s\"", + ssl->serverKey); + return false; + } + + if (!IS_EMPTY_STRING_BUFFER(ssl->caFile) && !file_exists(ssl->caFile)) + { + log_error("--ssl-ca-file file does not exist at \"%s\"", + ssl->caFile); + return false; + } + + if (!IS_EMPTY_STRING_BUFFER(ssl->crlFile) && !file_exists(ssl->crlFile)) + { + log_error("--ssl-crl-file file does not exist at \"%s\"", + ssl->crlFile); + return false; + } + + /* install a default value for --ssl-mode, use verify-full */ + if (ssl->sslMode == SSL_MODE_UNKNOWN) + { + ssl->sslMode = SSL_MODE_VERIFY_FULL; + strlcpy(ssl->sslModeStr, + pgsetup_sslmode_to_string(ssl->sslMode), SSL_MODE_STRLEN); + log_info("Using default --ssl-mode \"%s\"", ssl->sslModeStr); + } + + /* check that we have a CA file to use with verif-ca/verify-full */ + if (ssl->sslMode >= SSL_MODE_VERIFY_CA && IS_EMPTY_STRING_BUFFER(ssl->caFile)) + { + log_error("--ssl-ca-file is required when --ssl-mode \"%s\" is used", + ssl->sslModeStr); + return false; + } + + /* + * Normalize the filenames. + * We already log errors so we can simply return the result + */ + return normalize_filename(pgSetup->ssl.caFile, pgSetup->ssl.caFile, + MAXPGPATH) && + normalize_filename(pgSetup->ssl.crlFile, pgSetup->ssl.crlFile, + MAXPGPATH) && + normalize_filename(pgSetup->ssl.serverCert, pgSetup->ssl.serverCert, + MAXPGPATH) && + normalize_filename(pgSetup->ssl.serverKey, pgSetup->ssl.serverKey, + MAXPGPATH); + } + + /* + * When --ssl-self-signed is used, we default to using sslmode=require. + * Setting higher than that are wrong, false sense of security. + */ + if (ssl->createSelfSignedCert) + { + /* in that case we want an sslMode of require at most */ + if (ssl->sslMode > SSL_MODE_REQUIRE) + { + log_error("--ssl-mode \"%s\" is not compatible with self-signed " + "certificates, please provide certificates signed by " + "your trusted CA.", + pgsetup_sslmode_to_string(ssl->sslMode)); + log_info("See https://www.postgresql.org/docs/current/libpq-ssl.html" + " for details"); + return false; + } + + if (ssl->sslMode == SSL_MODE_UNKNOWN) + { + /* install a default value for --ssl-mode */ + ssl->sslMode = SSL_MODE_REQUIRE; + strlcpy(ssl->sslModeStr, + pgsetup_sslmode_to_string(ssl->sslMode), SSL_MODE_STRLEN); + log_info("Using default --ssl-mode \"%s\"", ssl->sslModeStr); + } + + log_info("Using --ssl-self-signed: pg_autoctl will " + "create self-signed certificates, allowing for " + "encrypted network traffic"); + log_warn("Self-signed certificates provide protection against " + "eavesdropping; this setup does NOT protect against " + "Man-In-The-Middle attacks nor Impersonation attacks."); + log_warn("See https://www.postgresql.org/docs/current/libpq-ssl.html " + "for details"); + + return true; + } + + /* --no-ssl is ok */ + if (ssl->active == 0) + { + log_warn("No encryption is used for network traffic! This allows an " + "attacker on the network to read all replication data."); + log_warn("Using --ssl-self-signed instead of --no-ssl is recommend to " + "achieve more security with the same ease of deployment."); + log_warn("See https://www.postgresql.org/docs/current/libpq-ssl.html " + "for details on how to improve"); + + /* Install a default value for --ssl-mode */ + if (ssl->sslMode == SSL_MODE_UNKNOWN) + { + ssl->sslMode = SSL_MODE_PREFER; + strlcpy(ssl->sslModeStr, + pgsetup_sslmode_to_string(ssl->sslMode), SSL_MODE_STRLEN); + log_info("Using default --ssl-mode \"%s\"", ssl->sslModeStr); + } + return true; + } + + return false; +} + + +/* + * pg_setup_sslmode_to_string parses a string representing the sslmode into an + * internal enum value, so that we can easily compare values. + */ +SSLMode +pgsetup_parse_sslmode(const char *sslMode) +{ + SSLMode enumArray[] = { + SSL_MODE_DISABLE, + SSL_MODE_ALLOW, + SSL_MODE_PREFER, + SSL_MODE_REQUIRE, + SSL_MODE_VERIFY_CA, + SSL_MODE_VERIFY_FULL + }; + + char *sslModeArray[] = { + "disable", "allow", "prefer", "require", + "verify-ca", "verify-full", NULL + }; + + int sslModeArrayIndex = 0; + + for (sslModeArrayIndex = 0; + sslModeArray[sslModeArrayIndex] != NULL; + sslModeArrayIndex++) + { + if (strcmp(sslMode, sslModeArray[sslModeArrayIndex]) == 0) + { + return enumArray[sslModeArrayIndex]; + } + } + + return SSL_MODE_UNKNOWN; +} + + +/* + * pgsetup_sslmode_to_string returns the string representation of the enum. + */ +char * +pgsetup_sslmode_to_string(SSLMode sslMode) +{ + switch (sslMode) + { + case SSL_MODE_UNKNOWN: + { + return "unknown"; + } + + case SSL_MODE_DISABLE: + { + return "disable"; + } + + case SSL_MODE_ALLOW: + { + return "allow"; + } + + case SSL_MODE_PREFER: + { + return "prefer"; + } + + case SSL_MODE_REQUIRE: + { + return "require"; + } + + case SSL_MODE_VERIFY_CA: + { + return "verify-ca"; + } + + case SSL_MODE_VERIFY_FULL: + { + return "verify-full"; + } + } + + /* This is a huge bug */ + log_error("BUG: some unknown SSL_MODE enum value was encountered"); + return "unknown"; +} + + +/* + * pg_setup_standby_slot_supported returns true when the target Postgres + * instance represented in pgSetup is compatible with using + * pg_replication_slot_advance() on a standby node. + * + * In Postgres 11 and 12, the pg_replication_slot_advance() function has been + * buggy and prevented WAL recycling on standby nodes. + * + * See https://github.com/hapostgres/pg_auto_failover/issues/283 for the problem + * and https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=b48df81 + * for the solution. + * + * We need Postgres 11 starting at 11.9, Postgres 12 starting at 12.4, or + * Postgres 13 or more recent to make use of pg_replication_slot_advance. + */ +bool +pg_setup_standby_slot_supported(PostgresSetup *pgSetup, int logLevel) +{ + int pg_version = 0; + + if (!parse_pg_version_string(pgSetup->pg_version, &pg_version)) + { + /* errors have already been logged */ + return false; + } + + int major = pg_version / 100; + int minor = pg_version % 100; + + /* do we have Postgres 10 (or before, though we don't support that) */ + if (pg_version < 1100) + { + log_trace("pg_setup_standby_slot_supported(%d): false", pg_version); + return false; + } + + /* Postgres 11.0 up to 11.8 included the bug */ + if (pg_version >= 1100 && pg_version < 1109) + { + log_level(logLevel, + "Postgres %d.%d does not support replication slots " + "on a standby node", major, minor); + + return false; + } + + /* Postgres 11.9 and up are good */ + if (pg_version >= 1109 && pg_version < 1200) + { + return true; + } + + /* Postgres 12.0 up to 12.3 included the bug */ + if (pg_version >= 1200 && pg_version < 1204) + { + log_level(logLevel, + "Postgres %d.%d does not support replication slots " + "on a standby node", major, minor); + + return false; + } + + /* Postgres 12.4 and up are good */ + if (pg_version >= 1204 && pg_version < 1300) + { + return true; + } + + /* Starting with Postgres 13, all versions are known to have the bug fix */ + if (pg_version >= 1300) + { + return true; + } + + /* should not happen */ + log_debug("BUG in pg_setup_standby_slot_supported(%d): " + "unknown Postgres version, returning false", + pg_version); + + return false; +} + + +/* + * pgsetup_parse_hba_level parses a string that represents an HBAEditLevel + * value. + */ +HBAEditLevel +pgsetup_parse_hba_level(const char *level) +{ + HBAEditLevel enumArray[] = { + HBA_EDIT_SKIP, + HBA_EDIT_MINIMAL, + HBA_EDIT_LAN + }; + + char *levelArray[] = { "skip", "minimal", "app", NULL }; + + for (int i = 0; levelArray[i] != NULL; i++) + { + if (strcmp(level, levelArray[i]) == 0) + { + return enumArray[i]; + } + } + + return HBA_EDIT_UNKNOWN; +} + + +/* + * pgsetup_hba_level_to_string returns the string representation of an + * hbaLevel enum value. + */ +char * +pgsetup_hba_level_to_string(HBAEditLevel hbaLevel) +{ + switch (hbaLevel) + { + case HBA_EDIT_SKIP: + { + return "skip"; + } + + case HBA_EDIT_MINIMAL: + { + return "minimal"; + } + + case HBA_EDIT_LAN: + { + return "app"; + } + + case HBA_EDIT_UNKNOWN: + { + return "unknown"; + } + } + + log_error("BUG: hbaLevel %d is unknown", hbaLevel); + return "unknown"; +} + + +/* + * dbstateToString returns a string from a pgControlFile state enum. + */ +const char * +dbstateToString(DBState state) +{ + switch (state) + { + case DB_STARTUP: + { + return "starting up"; + } + + case DB_SHUTDOWNED: + { + return "shut down"; + } + + case DB_SHUTDOWNED_IN_RECOVERY: + { + return "shut down in recovery"; + } + + case DB_SHUTDOWNING: + { + return "shutting down"; + } + + case DB_IN_CRASH_RECOVERY: + { + return "in crash recovery"; + } + + case DB_IN_ARCHIVE_RECOVERY: + { + return "in archive recovery"; + } + + case DB_IN_PRODUCTION: + { + return "in production"; + } + } + return "unrecognized status code"; +} diff --git a/src/bin/common/pgsetup.h b/src/bin/common/pgsetup.h new file mode 100644 index 000000000..f4d4b182d --- /dev/null +++ b/src/bin/common/pgsetup.h @@ -0,0 +1,282 @@ +/* + * src/bin/pg_autoctl/pgsetup.h + * Discovers a PostgreSQL setup by calling pg_controldata and reading + * postmaster.pid file, getting clues from the process environment and from + * user given hints (options). + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the PostgreSQL License. + * + */ + +#ifndef PGSETUP_H +#define PGSETUP_H + +#include + +#include "postgres_fe.h" + +#include "parson.h" + +/* + * Maximum length of serialized pg_lsn value + * It is taken from postgres file pg_lsn.c. + * It defines MAXPG_LSNLEN to be 17 and + * allocates a buffer 1 byte larger. We + * went for 18 to make buffer allocation simpler. + */ +#define PG_LSN_MAXLENGTH 18 + +/* + * System status indicator. From postgres:src/include/catalog/pg_control.h + */ +typedef enum DBState +{ + DB_STARTUP = 0, + DB_SHUTDOWNED, + DB_SHUTDOWNED_IN_RECOVERY, + DB_SHUTDOWNING, + DB_IN_CRASH_RECOVERY, + DB_IN_ARCHIVE_RECOVERY, + DB_IN_PRODUCTION +} DBState; + +/* + * To be able to check if a minor upgrade should be scheduled, and to check for + * system WAL compatiblity, we use some parts of the pg_controldata output. + * + * See postgresql/src/include/catalog/pg_control.h for definitions of the + * following fields of the ControlFileData struct. + */ +typedef struct pg_control_data +{ + uint64_t system_identifier; + uint32_t pg_control_version; /* PG_CONTROL_VERSION */ + uint32_t catalog_version_no; /* see catversion.h */ + DBState state; /* see enum above */ + char latestCheckpointLSN[PG_LSN_MAXLENGTH]; + uint32_t timeline_id; +} PostgresControlData; + +/* + * We don't need the full information set form the pidfile, it onyl allows us + * to guess/retrieve the PostgreSQL port number from the PGDATA without having + * to ask the user to provide the information. + */ +typedef struct pg_pidfile +{ + pid_t pid; + unsigned short port; +} PostgresPIDFile; + +/* + * From pidfile.h we also extract the Postmaster status, one of the following + * values: + */ +typedef enum +{ + POSTMASTER_STATUS_UNKNOWN = 0, + POSTMASTER_STATUS_STARTING, + POSTMASTER_STATUS_STOPPING, + POSTMASTER_STATUS_READY, + POSTMASTER_STATUS_STANDBY +} PostmasterStatus; + +/* + * When discovering Postgres we try to determine if the local $PGDATA directory + * belongs to a primary or a secondary server. If the server is running, it's + * easy: connect and ask with the pg_is_in_recovery() SQL function. If the + * server is not running, we might be lucky and find a standby setup file and + * then we know it's not a primary. + * + * Otherwise we just don't know. + */ +typedef enum PostgresRole +{ + POSTGRES_ROLE_UNKNOWN, + POSTGRES_ROLE_PRIMARY, + POSTGRES_ROLE_RECOVERY, /* Either PITR or Hot Standby */ + POSTGRES_ROLE_STANDBY /* We know it's an Hot Standby */ +} PostgresRole; + + +/* + * pg_auto_failover knows how to manage three kinds of PostgreSQL servers: + * + * - Standalone PostgreSQL instances + * - Citus Coordinator PostgreSQL instances + * - Citus Worker PostgreSQL instances + * + * Each of them may then take on the role of a primary or a standby depending + * on circumstances. Citus coordinator and worker instances need to load the + * citus extension in shared_preload_libraries, which the keeper ensures. + * + * At failover time, when dealing with a Citus worker instance, the keeper + * fetches its coordinator hostname and port from the monitor and blocks writes + * using the citus master_update_node() function call in a prepared + * transaction. + * + * We use the PgInstanceKind in the FSM as a guard to choose the transition + * function depending on the kind of node we are dealing with. We match the + * node kind using bitwise & operator, so we need to use powers of two here. + * Also we represent "any kind" with the 0xff value. Any all-one value (when + * seen in binary) larger than the max enum value will do really. + */ + +typedef enum PgInstanceKind +{ + NODE_KIND_UNKNOWN = 0, + NODE_KIND_STANDALONE = 1, + NODE_KIND_CITUS_COORDINATOR = 2, + NODE_KIND_CITUS_WORKER = 4, + + NODE_KIND_ANY = 0xff +} PgInstanceKind; + + +#define NODE_KIND_CITUS_ANY \ + (NODE_KIND_CITUS_COORDINATOR | NODE_KIND_CITUS_WORKER) + + +#define IS_CITUS_INSTANCE_KIND(x) \ + (x == NODE_KIND_CITUS_COORDINATOR \ + || x == NODE_KIND_CITUS_WORKER) + +#define pgKind_matches(x, y) ((x & y) != 0) + +#define PG_VERSION_STRING_MAX 12 + +/* + * Monitor keeps a replication settings for each node. + */ +typedef struct NodeReplicationSettings +{ + char name[_POSIX_HOST_NAME_MAX]; + int candidatePriority; /* promotion candidate priority */ + bool replicationQuorum; /* true if participates in write quorum */ +} NodeReplicationSettings; + + +/* + * How much should we edit the Postgres HBA file? + * + * The default value is HBA_EDIT_MINIMAL and pg_autoctl then add entries for + * the monitor to be able to connect to the local node, and an entry for the + * other nodes to be able to connect with streaming replication privileges. + */ +typedef enum +{ + HBA_EDIT_UNKNOWN = 0, + HBA_EDIT_SKIP, + HBA_EDIT_MINIMAL, + HBA_EDIT_LAN, +} HBAEditLevel; + +/* + * pg_auto_failover also support SSL settings. + */ +typedef enum +{ + SSL_MODE_UNKNOWN = 0, + SSL_MODE_DISABLE, + SSL_MODE_ALLOW, + SSL_MODE_PREFER, + SSL_MODE_REQUIRE, + SSL_MODE_VERIFY_CA, + SSL_MODE_VERIFY_FULL +} SSLMode; + +#define SSL_MODE_STRLEN 12 /* longuest is "verify-full" at 11 chars */ + +typedef struct SSLOptions +{ + int active; /* INI support has int, does not have bool */ + bool createSelfSignedCert; + SSLMode sslMode; + char sslModeStr[SSL_MODE_STRLEN]; + char caFile[MAXPGPATH]; + char crlFile[MAXPGPATH]; + char serverCert[MAXPGPATH]; + char serverKey[MAXPGPATH]; +} SSLOptions; + +/* + * In the PostgresSetup structure, we use pghost either as socket directory + * name or as a hostname. We could use MAXPGPATH rather than + * _POSIX_HOST_NAME_MAX chars in that name, but then again the hostname is + * part of a connection string that must be held in MAXCONNINFO. + * + * If you want to change pghost[_POSIX_HOST_NAME_MAX], keep that in mind! + */ +typedef struct pg_setup +{ + char pgdata[MAXPGPATH]; /* PGDATA */ + char pg_ctl[MAXPGPATH]; /* absolute path to pg_ctl */ + char pg_version[PG_VERSION_STRING_MAX]; /* pg_ctl --version */ + char username[NAMEDATALEN]; /* username, defaults to USER */ + char dbname[NAMEDATALEN]; /* dbname, defaults to PGDATABASE */ + char pghost[_POSIX_HOST_NAME_MAX]; /* local PGHOST to connect to */ + int pgport; /* PGPORT */ + char listen_addresses[MAXPGPATH]; /* listen_addresses */ + int proxyport; /* Proxy port */ + char authMethod[NAMEDATALEN]; /* auth method, defaults to trust */ + char hbaLevelStr[NAMEDATALEN]; /* user choice of HBA editing */ + HBAEditLevel hbaLevel; /* user choice of HBA editing */ + PostmasterStatus pm_status; /* Postmaster status */ + bool is_in_recovery; /* select pg_is_in_recovery() */ + PostgresControlData control; /* pg_controldata pgdata */ + PostgresPIDFile pidFile; /* postmaster.pid information */ + PgInstanceKind pgKind; /* standalone/coordinator/worker */ + NodeReplicationSettings settings; /* node replication settings */ + SSLOptions ssl; /* ssl options */ + char citusClusterName[NAMEDATALEN]; /* citus.cluster_name */ +} PostgresSetup; + +#define IS_EMPTY_STRING_BUFFER(strbuf) (strbuf[0] == '\0') + +bool pg_setup_init(PostgresSetup *pgSetup, + PostgresSetup *options, + bool missing_pgdata_is_ok, + bool pg_is_not_running_is_ok); + +bool read_pg_pidfile(PostgresSetup *pgSetup, + bool pgIsNotRunningIsOk, + int maxRetries); + +void fprintf_pg_setup(FILE *stream, PostgresSetup *pgSetup); +bool pg_setup_as_json(PostgresSetup *pgSetup, JSON_Value *js); + +bool pg_setup_get_local_connection_string(PostgresSetup *pgSetup, + char *connectionString); +bool pg_setup_pgdata_exists(PostgresSetup *pgSetup); +bool pg_setup_is_running(PostgresSetup *pgSetup); +PostgresRole pg_setup_role(PostgresSetup *pgSetup); +bool pg_setup_is_ready(PostgresSetup *pgSetup, bool pg_is_not_running_is_ok); +bool pg_setup_wait_until_is_ready(PostgresSetup *pgSetup, + int timeout, int logLevel); +bool pg_setup_wait_until_is_stopped(PostgresSetup *pgSetup, + int timeout, int logLevel); +char * pmStatusToString(PostmasterStatus pm_status); + +char * pg_setup_get_username(PostgresSetup *pgSetup); + +char * pg_setup_get_auth_method(PostgresSetup *pgSetup); +bool pg_setup_skip_hba_edits(PostgresSetup *pgSetup); + +bool pg_setup_set_absolute_pgdata(PostgresSetup *pgSetup); + +PgInstanceKind nodeKindFromString(const char *nodeKind); +char * nodeKindToString(PgInstanceKind kind); +int pgsetup_get_pgport(void); + +bool pgsetup_validate_ssl_settings(PostgresSetup *pgSetup); +SSLMode pgsetup_parse_sslmode(const char *sslMode); +char * pgsetup_sslmode_to_string(SSLMode sslMode); + +bool pg_setup_standby_slot_supported(PostgresSetup *pgSetup, int logLevel); + +HBAEditLevel pgsetup_parse_hba_level(const char *level); +char * pgsetup_hba_level_to_string(HBAEditLevel hbaLevel); +const char * dbstateToString(DBState state); + +#endif /* PGSETUP_H */ diff --git a/src/bin/common/pgsql.c b/src/bin/common/pgsql.c new file mode 100644 index 000000000..4fe32a084 --- /dev/null +++ b/src/bin/common/pgsql.c @@ -0,0 +1,3470 @@ +/* + * src/bin/pg_autoctl/pgsql.c + * API for sending SQL commands to a PostgreSQL server + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the PostgreSQL License. + * + */ +#include +#include +#include + +#include "postgres.h" +#include "postgres_fe.h" +#include "libpq-fe.h" +#include "pqexpbuffer.h" +#include "portability/instr_time.h" + +#if PG_MAJORVERSION_NUM >= 15 +#include "common/pg_prng.h" +#endif + +#include "cli_root.h" +#include "defaults.h" +#include "log.h" +#include "parsing.h" +#include "pgsql.h" +#include "signals.h" +#include "string_utils.h" + + +#define STR_ERRCODE_DUPLICATE_OBJECT "42710" +#define STR_ERRCODE_DUPLICATE_DATABASE "42P04" + +#define STR_ERRCODE_INVALID_OBJECT_DEFINITION "42P17" +#define STR_ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE "55000" +#define STR_ERRCODE_OBJECT_IN_USE "55006" +#define STR_ERRCODE_UNDEFINED_OBJECT "42704" + +static char * ConnectionTypeToString(ConnectionType connectionType); +static void log_connection_error(PGconn *connection, int logLevel); +static void pgAutoCtlDefaultNoticeProcessor(void *arg, const char *message); +static void pgAutoCtlDebugNoticeProcessor(void *arg, const char *message); +static PGconn * pgsql_open_connection(PGSQL *pgsql); +static bool pgsql_retry_open_connection(PGSQL *pgsql); +static bool is_response_ok(PGresult *result); +static bool clear_results(PGSQL *pgsql); +static void pgsql_handle_notifications(PGSQL *pgsql); +static bool pgsql_alter_system_set(PGSQL *pgsql, GUC setting); +static bool pgsql_get_current_setting(PGSQL *pgsql, char *settingName, + char **currentValue); +static void parsePgMetadata(void *ctx, PGresult *result); +static void parsePgReachedTargetLSN(void *ctx, PGresult *result); +static void parseReplicationSlotMaintain(void *ctx, PGresult *result); +static void parsePgReachedTargetLSN(void *ctx, PGresult *result); +static void parseIdentifySystemResult(void *ctx, PGresult *result); +static void parseTimelineHistoryResult(void *ctx, PGresult *result); + + +/* + * parseSingleValueResult is a ParsePostgresResultCB callback that reads the + * first column of the first row of the resultset only, and parses the answer + * into the expected C value, one of type QueryResultType. + */ +void +parseSingleValueResult(void *ctx, PGresult *result) +{ + SingleValueResultContext *context = (SingleValueResultContext *) ctx; + + context->ntuples = PQntuples(result); + + if (context->ntuples == 1) + { + char *value = PQgetvalue(result, 0, 0); + + /* this function is never used when we expect NULL values */ + if (PQgetisnull(result, 0, 0)) + { + context->parsedOk = false; + return; + } + + switch (context->resultType) + { + case PGSQL_RESULT_BOOL: + { + context->boolVal = strcmp(value, "t") == 0; + context->parsedOk = true; + break; + } + + case PGSQL_RESULT_INT: + { + if (!stringToInt(value, &context->intVal)) + { + context->parsedOk = false; + log_error("Failed to parse int result \"%s\"", value); + } + context->parsedOk = true; + break; + } + + case PGSQL_RESULT_BIGINT: + { + if (!stringToUInt64(value, &context->bigint)) + { + context->parsedOk = false; + log_error("Failed to parse uint64_t result \"%s\"", value); + } + context->parsedOk = true; + break; + } + + case PGSQL_RESULT_STRING: + { + context->strVal = strdup(value); + if (context->strVal == NULL) + { + context->parsedOk = false; + log_error(ALLOCATION_FAILED_ERROR); + } + context->parsedOk = true; + break; + } + } + } +} + + +/* + * fetchedRows is a pgsql_execute_with_params callback function that sets a + * SingleValueResultContext->intVal to PQntuples(result), that is how many rows + * are fetched by the query. + */ +void +fetchedRows(void *ctx, PGresult *result) +{ + SingleValueResultContext *context = (SingleValueResultContext *) ctx; + + context->parsedOk = true; + context->intVal = PQntuples(result); +} + + +/* + * pgsql_init initializes a PGSQL struct to connect to the given database + * URL or connection string. + */ +bool +pgsql_init(PGSQL *pgsql, char *url, ConnectionType connectionType) +{ + pgsql->connectionType = connectionType; + pgsql->connection = NULL; + + /* set our default retry policy for interactive commands */ + (void) pgsql_set_interactive_retry_policy(&(pgsql->retryPolicy)); + + if (validate_connection_string(url)) + { + /* size of url has already been validated. */ + strlcpy(pgsql->connectionString, url, MAXCONNINFO); + } + else + { + return false; + } + return true; +} + + +/* + * pgsql_set_retry_policy sets the retry policy to the given maxT (maximum + * total time spent retrying), maxR (maximum number of retries, zero when not + * retrying at all, -1 for unbounded number of retries), and maxSleepTime to + * cap our exponential backoff with decorrelated jitter computation. + */ +void +pgsql_set_retry_policy(ConnectionRetryPolicy *retryPolicy, + int maxT, + int maxR, + int maxSleepTime, + int baseSleepTime) +{ + retryPolicy->maxT = maxT; + retryPolicy->maxR = maxR; + retryPolicy->maxSleepTime = maxSleepTime; + retryPolicy->baseSleepTime = baseSleepTime; + + /* initialize a seed for our random number generator */ +#if PG_MAJORVERSION_NUM < 15 + pg_srand48(time(0)); +#else + pg_prng_seed(&(retryPolicy->prng_state), (uint64) (getpid() ^ time(NULL))); +#endif +} + + +/* + * pgsql_set_default_retry_policy sets the default retry policy: no retry. We + * use the other default parameters but with a maxR of zero they don't get + * used. + * + * This is the retry policy that prevails in the main keeper loop. + */ +void +pgsql_set_main_loop_retry_policy(ConnectionRetryPolicy *retryPolicy) +{ + (void) pgsql_set_retry_policy(retryPolicy, + POSTGRES_PING_RETRY_TIMEOUT, + 0, /* do not retry by default */ + POSTGRES_PING_RETRY_CAP_SLEEP_TIME, + POSTGRES_PING_RETRY_BASE_SLEEP_TIME); +} + + +/* + * pgsql_set_init_retry_policy sets the retry policy to 15 mins of total + * retrying time, unbounded number of attempts, and up to 2 seconds of sleep + * time in between attempts. + * + * This is the policy that we use in keeper_register_and_init. When using + * automated provisioning tools and frameworks, it might be that every node is + * provisionned concurrently and we might try to connect to the monitor before + * it's ready. In that case we want to retry for a long time. + */ +void +pgsql_set_init_retry_policy(ConnectionRetryPolicy *retryPolicy) +{ + (void) pgsql_set_retry_policy(retryPolicy, + POSTGRES_PING_RETRY_TIMEOUT, + -1, /* unbounded number of attempts */ + POSTGRES_PING_RETRY_CAP_SLEEP_TIME, + POSTGRES_PING_RETRY_BASE_SLEEP_TIME); +} + + +/* + * pgsql_set_interactive_retry_policy sets the retry policy to 2 seconds of + * total retrying time (or PGCONNECT_TIMEOUT when that's set), unbounded number + * of attempts, and up to 2 seconds of sleep time in between attempts. + */ +void +pgsql_set_interactive_retry_policy(ConnectionRetryPolicy *retryPolicy) +{ + (void) pgsql_set_retry_policy(retryPolicy, + pgconnect_timeout, + -1, /* unbounded number of attempts */ + POSTGRES_PING_RETRY_CAP_SLEEP_TIME, + POSTGRES_PING_RETRY_BASE_SLEEP_TIME); +} + + +/* + * pgsql_set_monitor_interactive_retry_policy sets the retry policy to 15 mins + * of total retrying time, unbounded number of attemps, and up to 5 seconds of + * sleep time in between attemps, starting at 1 second for the first retry. + * + * We use this policy in interactive commands when connecting to the monitor, + * such as when doing pg_autoctl enable|disable maintenance. + */ +void +pgsql_set_monitor_interactive_retry_policy(ConnectionRetryPolicy *retryPolicy) +{ + int cap = 5 * 1000; /* sleep up to 5s between attempts */ + int sleepTime = 1 * 1000; /* first retry happens after 1 second */ + + (void) pgsql_set_retry_policy(retryPolicy, + POSTGRES_PING_RETRY_TIMEOUT, + -1, /* unbounded number of attempts */ + cap, + sleepTime); +} + + +#define min(a, b) (a < b ? a : b) + +/* + * http://c-faq.com/lib/randrange.html + */ +#define random_between(R, M, N) ((M) + R / (RAND_MAX / ((N) -(M) +1) + 1)) + +/* + * pick_random_sleep_time picks a random sleep time between the given policy + * base sleep time and 3 times the previous sleep time. See below in + * pgsql_compute_connection_retry_sleep_time for a deep dive into why we are + * interested in this computation. + */ +static int +pick_random_sleep_time(ConnectionRetryPolicy *retryPolicy) +{ +#if PG_MAJORVERSION_NUM < 15 + long random = pg_lrand48(); +#else + uint32_t random = pg_prng_uint32(&(retryPolicy->prng_state)); +#endif + + return random_between(random, + retryPolicy->baseSleepTime, + retryPolicy->sleepTime * 3); +} + + +/* + * pgsql_compute_connection_retry_sleep_time returns how much time to sleep + * this time, in milliseconds. + */ +int +pgsql_compute_connection_retry_sleep_time(ConnectionRetryPolicy *retryPolicy) +{ + /* + * https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/ + * + * Adding jitter is a small change to the sleep function: + * + * sleep = random_between(0, min(cap, base * 2^attempt)) + * + * There are a few ways to implement these timed backoff loops. Let’s call + * the algorithm above “Full Jitter”, and consider two alternatives. The + * first alternative is “Equal Jitter”, where we always keep some of the + * backoff and jitter by a smaller amount: + * + * temp = min(cap, base * 2^attempt) + * sleep = temp/2 + random_between(0, temp/2) + * + * The intuition behind this one is that it prevents very short sleeps, + * always keeping some of the slow down from the backoff. + * + * A second alternative is “Decorrelated Jitter”, which is similar to “Full + * Jitter”, but we also increase the maximum jitter based on the last + * random value. + * + * sleep = min(cap, random_between(base, sleep*3)) + * + * Which approach do you think is best? + * + * The no-jitter exponential backoff approach is the clear loser. [...] + * + * Of the jittered approaches, “Equal Jitter” is the loser. It does + * slightly more work than “Full Jitter”, and takes much longer. The + * decision between “Decorrelated Jitter” and “Full Jitter” is less clear. + * The “Full Jitter” approach uses less work, but slightly more time. Both + * approaches, though, present a substantial decrease in client work and + * server load. + * + * Here we implement "Decorrelated Jitter", which is better in terms of + * time spent, something we care to optimize for even when it means more + * work on the monitor side. + */ + int sleepTime = pick_random_sleep_time(retryPolicy); + + retryPolicy->sleepTime = min(retryPolicy->maxSleepTime, sleepTime); + + ++(retryPolicy->attempts); + + return retryPolicy->sleepTime; +} + + +/* + * pgsql_retry_policy_expired returns true when we should stop retrying, either + * per the policy (maxR / maxT) or because we received a signal that we have to + * obey. + */ +bool +pgsql_retry_policy_expired(ConnectionRetryPolicy *retryPolicy) +{ + instr_time duration; + + /* Any signal is reason enough to break out from this retry loop. */ + if (asked_to_quit || asked_to_stop || asked_to_stop_fast || asked_to_reload) + { + return true; + } + + /* set the first retry time when it's not been set previously */ + if (INSTR_TIME_IS_ZERO(retryPolicy->startTime)) + { + INSTR_TIME_SET_CURRENT(retryPolicy->startTime); + } + + INSTR_TIME_SET_CURRENT(duration); + INSTR_TIME_SUBTRACT(duration, retryPolicy->startTime); + + /* + * We stop retrying as soon as we have spent all of our time budget or all + * of our attempts count budget, whichever comes first. + * + * maxR = 0 (zero) means no retry at all, checked before the loop + * maxR < 0 (zero) means unlimited number of retries + */ + if ((INSTR_TIME_GET_MILLISEC(duration) >= (retryPolicy->maxT * 1000)) || + (retryPolicy->maxR > 0 && + retryPolicy->attempts >= retryPolicy->maxR)) + { + return true; + } + + return false; +} + + +/* + * connectionTypeToString transforms a connectionType in a string to be used in + * a user facing message. + */ +static char * +ConnectionTypeToString(ConnectionType connectionType) +{ + switch (connectionType) + { + case PGSQL_CONN_LOCAL: + { + return "local"; + } + + case PGSQL_CONN_MONITOR: + { + return "monitor"; + } + + case PGSQL_CONN_COORDINATOR: + { + return "coordinator"; + } + + case PGSQL_CONN_UPSTREAM: + { + return "upstream"; + } + + default: + { + return "unknown connection type"; + } + } +} + + +/* + * Finish a PGSQL client connection. + */ +void +pgsql_finish(PGSQL *pgsql) +{ + if (pgsql->connection != NULL) + { + char scrubbedConnectionString[MAXCONNINFO] = { 0 }; + + if (!parse_and_scrub_connection_string(pgsql->connectionString, + scrubbedConnectionString)) + { + log_debug("Failed to scrub password from connection string"); + + strlcpy(scrubbedConnectionString, + pgsql->connectionString, + sizeof(scrubbedConnectionString)); + } + + log_debug("Disconnecting from [%s] \"%s\"", + ConnectionTypeToString(pgsql->connectionType), + scrubbedConnectionString); + PQfinish(pgsql->connection); + pgsql->connection = NULL; + + /* + * When we fail to connect, on the way out we call pgsql_finish to + * reset the connection to NULL. We still want the callers to be able + * to inquire about our connection status, so refrain to reset the + * status. + */ + } + + pgsql->connectionStatementType = PGSQL_CONNECTION_SINGLE_STATEMENT; +} + + +/* + * log_connection_error logs the PQerrorMessage from the given connection. + */ +static void +log_connection_error(PGconn *connection, int logLevel) +{ + char *message = connection != NULL ? PQerrorMessage(connection) : NULL; + char *errorLines[BUFSIZE] = { 0 }; + int lineCount = splitLines(message, errorLines, BUFSIZE); + int lineNumber = 0; + + /* PQerrorMessage is then "connection pointer is NULL", not helpful */ + if (connection == NULL) + { + return; + } + + for (lineNumber = 0; lineNumber < lineCount; lineNumber++) + { + char *line = errorLines[lineNumber]; + + if (lineNumber == 0) + { + log_level(logLevel, "Connection to database failed: %s", line); + } + else + { + log_level(logLevel, "%s", line); + } + } +} + + +/* + * pgsql_open_connection opens a PostgreSQL connection, given a PGSQL client + * instance. If a connection is already open in the client (it's not NULL), + * then this errors, unless we are inside a transaction opened by pgsql_begin. + */ +static PGconn * +pgsql_open_connection(PGSQL *pgsql) +{ + /* we might be connected already */ + if (pgsql->connection != NULL) + { + if (pgsql->connectionStatementType != PGSQL_CONNECTION_MULTI_STATEMENT) + { + log_error("BUG: requested to open an already open connection in " + "non PGSQL_CONNECTION_MULTI_STATEMENT mode"); + pgsql_finish(pgsql); + return NULL; + } + return pgsql->connection; + } + + char scrubbedConnectionString[MAXCONNINFO] = { 0 }; + + (void) parse_and_scrub_connection_string(pgsql->connectionString, + scrubbedConnectionString); + + log_debug("Connecting to [%s] \"%s\"", + ConnectionTypeToString(pgsql->connectionType), + scrubbedConnectionString); + + /* we implement our own retry strategy */ + setenv("PGCONNECT_TIMEOUT", POSTGRES_CONNECT_TIMEOUT, 1); + + /* register our starting time */ + INSTR_TIME_SET_CURRENT(pgsql->retryPolicy.startTime); + INSTR_TIME_SET_ZERO(pgsql->retryPolicy.connectTime); + + /* Make a connection to the database */ + pgsql->connection = PQconnectdb(pgsql->connectionString); + + /* Check to see that the backend connection was successfully made */ + if (PQstatus(pgsql->connection) != CONNECTION_OK) + { + /* + * Implement the retry policy: + * + * First observe the maxR property: maximum retries allowed. When set + * to zero, we don't retry at all. + */ + if (pgsql->retryPolicy.maxR == 0) + { + INSTR_TIME_SET_CURRENT(pgsql->retryPolicy.connectTime); + + (void) log_connection_error(pgsql->connection, LOG_ERROR); + + log_error("Failed to connect to %s database at \"%s\", " + "see above for details", + ConnectionTypeToString(pgsql->connectionType), + pgsql->connectionString); + + pgsql->status = PG_CONNECTION_BAD; + + pgsql_finish(pgsql); + return NULL; + } + + /* + * If we reach this part of the code, the connectionType is not LOCAL + * and the retryPolicy has a non-zero maximum retry count. Let's retry! + */ + if (!pgsql_retry_open_connection(pgsql)) + { + /* errors have already been logged */ + return NULL; + } + } + + INSTR_TIME_SET_CURRENT(pgsql->retryPolicy.connectTime); + pgsql->status = PG_CONNECTION_OK; + + /* set the libpq notice receiver to integrate notifications as warnings. */ + PQsetNoticeProcessor(pgsql->connection, + &pgAutoCtlDefaultNoticeProcessor, + NULL); + + return pgsql->connection; +} + + +/* + * Refrain from warning too often. The user certainly wants to know that we are + * still trying to connect, though warning several times a second is not going + * to help anyone. A good trade-off seems to be a warning every 30s. + */ +#define SHOULD_WARN_AGAIN(duration) \ + (INSTR_TIME_GET_MILLISEC(duration) > 30000) + +/* + * pgsql_retry_open_connection loops over a PQping call until the remote server + * is ready to accept connections, and then connects to it and returns true + * when it could connect, false otherwise. + */ +static bool +pgsql_retry_open_connection(PGSQL *pgsql) +{ + bool connectionOk = false; + + PGPing lastWarningMessage = PQPING_OK; + instr_time lastWarningTime; + + INSTR_TIME_SET_ZERO(lastWarningTime); + + char scrubbedConnectionString[MAXCONNINFO] = { 0 }; + + (void) parse_and_scrub_connection_string(pgsql->connectionString, + scrubbedConnectionString); + + log_warn("Failed to connect to \"%s\", retrying until " + "the server is ready", scrubbedConnectionString); + + /* should not happen */ + if (pgsql->retryPolicy.maxR == 0) + { + return false; + } + + /* reset our internal counter before entering the retry loop */ + pgsql->retryPolicy.attempts = 1; + + while (!connectionOk) + { + if (pgsql_retry_policy_expired(&(pgsql->retryPolicy))) + { + instr_time duration; + + INSTR_TIME_SET_CURRENT(duration); + INSTR_TIME_SUBTRACT(duration, pgsql->retryPolicy.startTime); + + (void) log_connection_error(pgsql->connection, LOG_ERROR); + pgsql->status = PG_CONNECTION_BAD; + pgsql_finish(pgsql); + + log_error("Failed to connect to \"%s\" " + "after %d attempts in %d ms, " + "pg_autoctl stops retrying now", + scrubbedConnectionString, + pgsql->retryPolicy.attempts, + (int) INSTR_TIME_GET_MILLISEC(duration)); + + return false; + } + + /* + * Now compute how much time to wait for this round, and increment how + * many times we tried to connect already. + */ + int sleep = + pgsql_compute_connection_retry_sleep_time(&(pgsql->retryPolicy)); + + /* we have milliseconds, pg_usleep() wants microseconds */ + (void) pg_usleep(sleep * 1000); + + log_debug("PQping(%s): slept %d ms on attempt %d", + scrubbedConnectionString, + pgsql->retryPolicy.sleepTime, + pgsql->retryPolicy.attempts); + + switch (PQping(pgsql->connectionString)) + { + /* + * https://www.postgresql.org/docs/current/libpq-connect.html + * + * The server is running and appears to be accepting connections. + */ + case PQPING_OK: + { + log_debug("PQping OK after %d attempts", + pgsql->retryPolicy.attempts); + + /* + * Ping is now ok, and connection is still NULL because the + * first attempt to connect failed. Now is a good time to + * establish the connection. + * + * PQping does not check authentication, so we might still fail + * to connect to the server. + */ + pgsql->connection = PQconnectdb(pgsql->connectionString); + + if (PQstatus(pgsql->connection) == CONNECTION_OK) + { + instr_time duration; + + INSTR_TIME_SET_CURRENT(duration); + + connectionOk = true; + pgsql->status = PG_CONNECTION_OK; + pgsql->retryPolicy.connectTime = duration; + + INSTR_TIME_SUBTRACT(duration, pgsql->retryPolicy.startTime); + + log_info("Successfully connected to \"%s\" " + "after %d attempts in %d ms.", + scrubbedConnectionString, + pgsql->retryPolicy.attempts, + (int) INSTR_TIME_GET_MILLISEC(duration)); + } + else + { + instr_time durationSinceLastWarning; + + INSTR_TIME_SET_CURRENT(durationSinceLastWarning); + INSTR_TIME_SUBTRACT(durationSinceLastWarning, lastWarningTime); + + if (lastWarningMessage != PQPING_OK || + SHOULD_WARN_AGAIN(durationSinceLastWarning)) + { + lastWarningMessage = PQPING_OK; + INSTR_TIME_SET_CURRENT(lastWarningTime); + + /* + * Only show details when that's the last attempt, + * otherwise accept that this may happen as a transient + * state. + */ + (void) log_connection_error(pgsql->connection, LOG_DEBUG); + + log_debug("Failed to connect after successful ping"); + } + } + break; + } + + /* + * https://www.postgresql.org/docs/current/libpq-connect.html + * + * The server is running but is in a state that disallows + * connections (startup, shutdown, or crash recovery). + */ + case PQPING_REJECT: + { + instr_time durationSinceLastWarning; + + INSTR_TIME_SET_CURRENT(durationSinceLastWarning); + INSTR_TIME_SUBTRACT(durationSinceLastWarning, lastWarningTime); + + if (lastWarningMessage != PQPING_REJECT || + SHOULD_WARN_AGAIN(durationSinceLastWarning)) + { + lastWarningMessage = PQPING_REJECT; + INSTR_TIME_SET_CURRENT(lastWarningTime); + + log_warn( + "The server at \"%s\" is running but is in a state " + "that disallows connections (startup, shutdown, or " + "crash recovery).", + scrubbedConnectionString); + } + + break; + } + + /* + * https://www.postgresql.org/docs/current/libpq-connect.html + * + * The server could not be contacted. This might indicate that the + * server is not running, or that there is something wrong with the + * given connection parameters (for example, wrong port number), or + * that there is a network connectivity problem (for example, a + * firewall blocking the connection request). + */ + case PQPING_NO_RESPONSE: + { + instr_time durationSinceStart, durationSinceLastWarning; + + INSTR_TIME_SET_CURRENT(durationSinceStart); + INSTR_TIME_SUBTRACT(durationSinceStart, + pgsql->retryPolicy.startTime); + + INSTR_TIME_SET_CURRENT(durationSinceLastWarning); + INSTR_TIME_SUBTRACT(durationSinceLastWarning, lastWarningTime); + + /* no message at all the first 30s: 30000ms */ + if (SHOULD_WARN_AGAIN(durationSinceStart) && + (lastWarningMessage != PQPING_NO_RESPONSE || + SHOULD_WARN_AGAIN(durationSinceLastWarning))) + { + lastWarningMessage = PQPING_NO_RESPONSE; + INSTR_TIME_SET_CURRENT(lastWarningTime); + + log_warn( + "The server at \"%s\" could not be contacted " + "after %d attempts in %d ms (milliseconds). " + "This might indicate that the server is not running, " + "or that there is something wrong with the given " + "connection parameters (for example, wrong port " + "number), or that there is a network connectivity " + "problem (for example, a firewall blocking the " + "connection request).", + scrubbedConnectionString, + pgsql->retryPolicy.attempts, + (int) INSTR_TIME_GET_MILLISEC(durationSinceStart)); + } + + break; + } + + /* + * https://www.postgresql.org/docs/current/libpq-connect.html + * + * No attempt was made to contact the server, because the supplied + * parameters were obviously incorrect or there was some + * client-side problem (for example, out of memory). + */ + case PQPING_NO_ATTEMPT: + { + lastWarningMessage = PQPING_NO_ATTEMPT; + log_debug("Failed to ping server \"%s\" because of " + "client-side problems (no attempt were made)", + scrubbedConnectionString); + break; + } + } + } + + if (!connectionOk && pgsql->connection != NULL) + { + INSTR_TIME_SET_CURRENT(pgsql->retryPolicy.connectTime); + + (void) log_connection_error(pgsql->connection, LOG_ERROR); + pgsql->status = PG_CONNECTION_BAD; + pgsql_finish(pgsql); + + return false; + } + + return true; +} + + +/* + * pgAutoCtlDefaultNoticeProcessor is our default PostgreSQL libpq Notice + * Processing: NOTICE, WARNING, HINT etc are processed as log_warn messages by + * default. + */ +static void +pgAutoCtlDefaultNoticeProcessor(void *arg, const char *message) +{ + char *m = strdup(message); + char *lines[BUFSIZE]; + int lineCount = splitLines(m, lines, BUFSIZE); + int lineNumber = 0; + + for (lineNumber = 0; lineNumber < lineCount; lineNumber++) + { + log_warn("%s", lines[lineNumber]); + } + + free(m); +} + + +/* + * pgAutoCtlDebugNoticeProcessor is our PostgreSQL libpq Notice Processing to + * use when wanting to send NOTICE, WARNING, HINT as log_debug messages. + */ +static void +pgAutoCtlDebugNoticeProcessor(void *arg, const char *message) +{ + char *m = strdup(message); + char *lines[BUFSIZE]; + int lineCount = splitLines(m, lines, BUFSIZE); + int lineNumber = 0; + + for (lineNumber = 0; lineNumber < lineCount; lineNumber++) + { + log_debug("%s", lines[lineNumber]); + } + + free(m); +} + + +/* + * pgsql_begin is responsible for opening a mutli statement connection and + * opening a transaction block by issuing a 'BEGIN' query. + */ +bool +pgsql_begin(PGSQL *pgsql) +{ + /* + * Indicate that we're running a transaction, so that the connection is not + * closed after each query automatically. It also allows us to detect bugs + * easily. We need to do this before executing BEGIN, because otherwise the + * connection is closed after the BEGIN statement automatically. + */ + pgsql->connectionStatementType = PGSQL_CONNECTION_MULTI_STATEMENT; + + if (!pgsql_execute(pgsql, "BEGIN")) + { + /* + * We need to manually call pgsql_finish to clean up here in case of + * this failure, because we have set the statement type to MULTI. + */ + pgsql_finish(pgsql); + return false; + } + + return true; +} + + +/* + * pgsql_rollback is responsible for issuing a 'ROLLBACK' query to an already + * opened transaction, usually via a previous pgsql_begin() command. + * + * It closes the connection but leaves the error contents, if any, for the user + * to examine should it is wished for. + */ +bool +pgsql_rollback(PGSQL *pgsql) +{ + bool result; + + if (pgsql->connectionStatementType != PGSQL_CONNECTION_MULTI_STATEMENT || + pgsql->connection == NULL) + { + log_error("BUG: call to pgsql_rollback without holding an open " + "multi statement connection"); + return false; + } + + result = pgsql_execute(pgsql, "ROLLBACK"); + + /* + * Connection might be be closed during the pgsql_execute(), notably in case + * of error. Be explicit and close it regardless though. + */ + if (pgsql->connection) + { + pgsql_finish(pgsql); + } + + return result; +} + + +/* + * pgsql_commit is responsible for issuing a 'COMMIT' query to an already + * opened transaction, usually via a previous pgsql_begin() command. + * + * It closes the connection but leaves the error contents, if any, for the user + * to examine should it is wished for. + */ +bool +pgsql_commit(PGSQL *pgsql) +{ + bool result; + + if (pgsql->connectionStatementType != PGSQL_CONNECTION_MULTI_STATEMENT || + pgsql->connection == NULL) + { + log_error("BUG: call to pgsql_commit() without holding an open " + "multi statement connection"); + if (pgsql->connection) + { + pgsql_finish(pgsql); + } + return false; + } + + result = pgsql_execute(pgsql, "COMMIT"); + + /* + * Connection might be be closed during the pgsql_execute(), notably in case + * of error. Be explicit and close it regardless though. + */ + if (pgsql->connection) + { + pgsql_finish(pgsql); + } + + return result; +} + + +/* + * pgsql_execute opens a connection, runs a given SQL command, and closes + * the connection again. + * + * We avoid persisting connection across multiple commands to simplify error + * handling. + */ +bool +pgsql_execute(PGSQL *pgsql, const char *sql) +{ + return pgsql_execute_with_params(pgsql, sql, 0, NULL, NULL, NULL, NULL); +} + + +/* + * pgsql_execute_with_params opens a connection, runs a given SQL command, + * and closes the connection again. + * + * We avoid persisting connection across multiple commands to simplify error + * handling. + */ +bool +pgsql_execute_with_params(PGSQL *pgsql, const char *sql, int paramCount, + const Oid *paramTypes, const char **paramValues, + void *context, ParsePostgresResultCB *parseFun) +{ + char debugParameters[BUFSIZE] = { 0 }; + PGresult *result = NULL; + + PGconn *connection = pgsql_open_connection(pgsql); + + if (connection == NULL) + { + return false; + } + + log_debug("%s;", sql); + + if (paramCount > 0) + { + int paramIndex = 0; + int remainingBytes = BUFSIZE; + char *writePointer = (char *) debugParameters; + + for (paramIndex = 0; paramIndex < paramCount; paramIndex++) + { + int bytesWritten = 0; + const char *value = paramValues[paramIndex]; + + if (paramIndex > 0) + { + bytesWritten = sformat(writePointer, remainingBytes, ", "); + remainingBytes -= bytesWritten; + writePointer += bytesWritten; + } + + if (value == NULL) + { + bytesWritten = sformat(writePointer, remainingBytes, "NULL"); + } + else + { + bytesWritten = + sformat(writePointer, remainingBytes, "'%s'", value); + } + remainingBytes -= bytesWritten; + writePointer += bytesWritten; + } + log_debug("%s", debugParameters); + } + + if (paramCount == 0) + { + result = PQexec(connection, sql); + } + else + { + result = PQexecParams(connection, sql, + paramCount, paramTypes, paramValues, + NULL, NULL, 0); + } + + if (!is_response_ok(result)) + { + char *sqlstate = PQresultErrorField(result, PG_DIAG_SQLSTATE); + char *message = PQerrorMessage(connection); + char *errorLines[BUFSIZE]; + int lineCount = splitLines(message, errorLines, BUFSIZE); + int lineNumber = 0; + + char *prefix = + pgsql->connectionType == PGSQL_CONN_MONITOR ? "Monitor" : "Postgres"; + + /* + * PostgreSQL Error message might contain several lines. Log each of + * them as a separate ERROR line here. + */ + for (lineNumber = 0; lineNumber < lineCount; lineNumber++) + { + log_error("%s %s", prefix, errorLines[lineNumber]); + } + + /* + * The monitor uses those error codes in situations we know how to + * handle, so if we have one of those, it's not a client-side error + * with a badly formed SQL query etc. + */ + if (pgsql->connectionType == PGSQL_CONN_MONITOR && + sqlstate != NULL && + !(strcmp(sqlstate, STR_ERRCODE_INVALID_OBJECT_DEFINITION) == 0 || + strcmp(sqlstate, STR_ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE) == 0 || + strcmp(sqlstate, STR_ERRCODE_OBJECT_IN_USE) == 0 || + strcmp(sqlstate, STR_ERRCODE_UNDEFINED_OBJECT) == 0)) + { + log_error("SQL query: %s", sql); + log_error("SQL params: %s", debugParameters); + } + else + { + log_debug("SQL query: %s", sql); + log_debug("SQL params: %s", debugParameters); + } + + /* now stash away the SQL STATE if any */ + if (context && sqlstate) + { + AbstractResultContext *ctx = (AbstractResultContext *) context; + + strlcpy(ctx->sqlstate, sqlstate, SQLSTATE_LENGTH); + } + + /* if we get a connection exception, track that */ + if (sqlstate && + strncmp(sqlstate, STR_ERRCODE_CLASS_CONNECTION_EXCEPTION, 2) == 0) + { + pgsql->status = PG_CONNECTION_BAD; + } + + PQclear(result); + clear_results(pgsql); + + /* + * Multi statements might want to ROLLBACK and hold to the open + * connection for a retry step. + */ + if (pgsql->connectionStatementType == PGSQL_CONNECTION_SINGLE_STATEMENT) + { + PQfinish(pgsql->connection); + pgsql->connection = NULL; + } + + return false; + } + + if (parseFun != NULL) + { + (*parseFun)(context, result); + } + + PQclear(result); + clear_results(pgsql); + if (pgsql->connectionStatementType == PGSQL_CONNECTION_SINGLE_STATEMENT) + { + PQfinish(pgsql->connection); + pgsql->connection = NULL; + } + + return true; +} + + +/* + * is_response_ok returns whether the query result is a correct response + * (not an error or failure). + */ +static bool +is_response_ok(PGresult *result) +{ + ExecStatusType resultStatus = PQresultStatus(result); + + return resultStatus == PGRES_SINGLE_TUPLE || resultStatus == PGRES_TUPLES_OK || + resultStatus == PGRES_COMMAND_OK; +} + + +/* + * clear_results consumes results on a connection until NULL is returned. + * If an error is returned it returns false. + */ +static bool +clear_results(PGSQL *pgsql) +{ + PGconn *connection = pgsql->connection; + + /* + * Per Postgres documentation: You should, however, remember to check + * PQnotifies after each PQgetResult or PQexec, to see if any + * notifications came in during the processing of the command. + * + * Before calling clear_results(), we called PQexecParams(). + */ + (void) pgsql_handle_notifications(pgsql); + + while (true) + { + PGresult *result = PQgetResult(connection); + + /* + * Per Postgres documentation: You should, however, remember to check + * PQnotifies after each PQgetResult or PQexec, to see if any + * notifications came in during the processing of the command. + * + * Here, we just called PQgetResult(). + */ + (void) pgsql_handle_notifications(pgsql); + + if (result == NULL) + { + break; + } + + if (!is_response_ok(result)) + { + log_error("Failure from Postgres: %s", PQerrorMessage(connection)); + + PQclear(result); + pgsql_finish(pgsql); + return false; + } + + PQclear(result); + } + + return true; +} + + +/* + * pgsql_handle_notifications check PQnotifies when a PGSQL notificationChannel + * has been set. Then if the parsed notification is from the + * notificationGroupId we set notificationReceived and also log the + * notification. + * + * This allow another part of the code to later know that some notifications + * have been received. + */ +static void +pgsql_handle_notifications(PGSQL *pgsql) +{ + PGconn *connection = pgsql->connection; + PGnotify *notify; + + if (pgsql->notificationProcessFunction == NULL) + { + return; + } + + PQconsumeInput(connection); + while ((notify = PQnotifies(connection)) != NULL) + { + log_trace("pgsql_handle_notifications: \"%s\"", notify->extra); + + if ((*pgsql->notificationProcessFunction)(pgsql->notificationGroupId, + pgsql->notificationNodeId, + notify->relname, + notify->extra)) + { + /* mark that we received some notifications */ + pgsql->notificationReceived = true; + } + + PQfreemem(notify); + PQconsumeInput(connection); + } +} + + +/* + * pgsql_is_in_recovery connects to PostgreSQL and sets the is_in_recovery + * boolean to the result of the SELECT pg_is_in_recovery() query. It returns + * false when something went wrong doing that. + */ +bool +pgsql_is_in_recovery(PGSQL *pgsql, bool *is_in_recovery) +{ + SingleValueResultContext context = { { 0 }, PGSQL_RESULT_BOOL, false }; + char *sql = "SELECT pg_is_in_recovery()"; + + if (!pgsql_execute_with_params(pgsql, sql, 0, NULL, NULL, + &context, &parseSingleValueResult)) + { + /* errors have been logged already */ + return false; + } + + if (!context.parsedOk) + { + log_error("Failed to get result from pg_is_in_recovery()"); + return false; + } + + *is_in_recovery = context.boolVal; + + return true; +} + + +/* + * check_postgresql_settings connects to our local PostgreSQL instance and + * verifies that our minimal viable configuration is in place by running a SQL + * query that looks at the current settings. + */ +bool +pgsql_check_postgresql_settings(PGSQL *pgsql, bool isCitusInstanceKind, + bool *settings_are_ok) +{ + SingleValueResultContext context = { { 0 }, PGSQL_RESULT_BOOL, false }; + const char *sql = + isCitusInstanceKind ? + CHECK_CITUS_NODE_SETTINGS_SQL : CHECK_POSTGRESQL_NODE_SETTINGS_SQL; + + if (!pgsql_execute_with_params(pgsql, sql, 0, NULL, NULL, + &context, &parseSingleValueResult)) + { + /* errors have been logged already */ + return false; + } + + if (!context.parsedOk) + { + /* errors have already been logged */ + return false; + } + + *settings_are_ok = context.boolVal; + + return true; +} + + +/* + * pgsql_check_monitor_settings connects to the given pgsql instance to check + * that pgautofailover is part of shared_preload_libraries. + */ +bool +pgsql_check_monitor_settings(PGSQL *pgsql, bool *settings_are_ok) +{ + SingleValueResultContext context = { { 0 }, PGSQL_RESULT_BOOL, false }; + const char *sql = + "select exists(select 1 from " + "unnest(" + "string_to_array(current_setting('shared_preload_libraries'), ','))" + " as t(name) " + "where trim(name) = 'pgautofailover');"; + + if (!pgsql_execute_with_params(pgsql, sql, 0, NULL, NULL, + &context, &parseSingleValueResult)) + { + /* errors have been logged already */ + return false; + } + + if (!context.parsedOk) + { + /* errors have already been logged */ + return false; + } + + *settings_are_ok = context.boolVal; + + return true; +} + + +/* + * postgres_sprintf_replicationSlotName prints the replication Slot Name to use + * for given nodeId in the given slotName buffer of given size. + */ +bool +postgres_sprintf_replicationSlotName(int64_t nodeId, char *slotName, int size) +{ + int bytesWritten = + sformat(slotName, size, "%s_%" PRId64, + REPLICATION_SLOT_NAME_DEFAULT, + nodeId); + + return bytesWritten <= size; +} + + +/* + * pgsql_set_synchronous_standby_names set synchronous_standby_names on the + * local Postgres to the value computed on the pg_auto_failover monitor. + */ +bool +pgsql_set_synchronous_standby_names(PGSQL *pgsql, + char *synchronous_standby_names) +{ + char quoted[BUFSIZE] = { 0 }; + GUC setting = { "synchronous_standby_names", quoted }; + + if (sformat(quoted, BUFSIZE, "'%s'", synchronous_standby_names) >= BUFSIZE) + { + log_error("Failed to apply the synchronous_standby_names value \"%s\": " + "pg_autoctl supports values up to %d bytes and this one " + "requires %lu bytes", + synchronous_standby_names, + BUFSIZE, + (unsigned long) strlen(synchronous_standby_names)); + return false; + } + + return pgsql_alter_system_set(pgsql, setting); +} + + +/* + * pgsql_replication_slot_maintain advances the current confirmed position of + * the given replication slot up to the given LSN position, create the + * replication slot if it does not exist yet, and remove the slots that exist + * in Postgres but are ommited in the given array of slots. + */ +typedef struct ReplicationSlotMaintainContext +{ + char sqlstate[SQLSTATE_LENGTH]; + char operation[NAMEDATALEN]; + char slotName[BUFSIZE]; + char lsn[PG_LSN_MAXLENGTH]; + bool parsedOK; +} ReplicationSlotMaintainContext; + + +/* + * pgsql_replication_slot_exists checks that a replication slot with the given + * slotName exists on the Postgres server. + */ +bool +pgsql_replication_slot_exists(PGSQL *pgsql, const char *slotName, + bool *slotExists) +{ + SingleValueResultContext context = { { 0 }, PGSQL_RESULT_BOOL, false }; + char *sql = "SELECT 1 FROM pg_replication_slots WHERE slot_name = $1"; + int paramCount = 1; + Oid paramTypes[1] = { NAMEOID }; + const char *paramValues[1] = { slotName }; + + if (!pgsql_execute_with_params(pgsql, sql, + paramCount, paramTypes, paramValues, + &context, &fetchedRows)) + { + /* errors have already been logged */ + return false; + } + + if (!context.parsedOk) + { + log_error("Failed to check if the replication slot \"%s\" exists", + slotName); + return false; + } + + /* we receive 0 rows in the result when the slot does not exist yet */ + *slotExists = context.intVal == 1; + + return true; +} + + +/* + * pgsql_create_replication_slot tries to create a replication slot on the + * database identified by a connection string. It's implemented as CREATE IF + * NOT EXISTS so that it's idempotent and can be retried easily. + */ +bool +pgsql_create_replication_slot(PGSQL *pgsql, const char *slotName) +{ + ReplicationSlotMaintainContext context = { 0 }; + char *sql = + "SELECT 'create', slot_name, lsn " + " FROM pg_create_physical_replication_slot($1) " + " WHERE NOT EXISTS " + " (SELECT 1 FROM pg_replication_slots WHERE slot_name = $1)"; + const Oid paramTypes[1] = { TEXTOID }; + const char *paramValues[1] = { slotName }; + + log_trace("pgsql_create_replication_slot"); + + /* + * parseReplicationSlotMaintain will log_info() the replication slot + * creation if it happens. When the slot already exists we return 0 row and + * remain silent about it. + */ + return pgsql_execute_with_params(pgsql, sql, 1, paramTypes, paramValues, + &context, parseReplicationSlotMaintain); +} + + +/* + * pgsql_drop_replication_slot drops a given replication slot. + */ +bool +pgsql_drop_replication_slot(PGSQL *pgsql, const char *slotName) +{ + char *sql = + "SELECT pg_drop_replication_slot(slot_name) " + " FROM pg_replication_slots " + " WHERE slot_name = $1"; + Oid paramTypes[1] = { TEXTOID }; + const char *paramValues[1] = { slotName }; + + log_info("Drop replication slot \"%s\"", slotName); + + return pgsql_execute_with_params(pgsql, sql, + 1, paramTypes, paramValues, NULL, NULL); +} + + +/* + * BuildNodesArrayValues build the SQL expression to use in a FROM clause to + * represent the list of other standby nodes from the given nodeArray. + * + * Such a list looks either like: + * + * VALUES($1, $2::pg_lsn), ($3, $4) + * + * or for an empty set (e.g. when we're the only standby): + * + * SELECT id, lsn + * FROM (values (null::int, null::pg_lsn)) as t(id, lsn) + * WHERE false + * + * We actually need to provide an empty set (0 rows) with columns of the + * expected data types so that we can join against the existing replication + * slots and drop them. If the set is empty, we drop all the slots. + * + * We return how many parameters we filled in paramTypes and paramValues from + * the nodeArray. + */ +#define NODEID_MAX_LENGTH 20 /* should allow for bigint digits */ + +typedef struct nodesArraysValuesParams +{ + int count; + Oid types[NODE_ARRAY_MAX_COUNT * 2]; + char *values[NODE_ARRAY_MAX_COUNT * 2]; + + /* + * Pre-allocate arrays for the data separately from the values array, which + * needs to be a (const char **) thing rather than a (char [][]) thing, + * because of the pgsql_execute_with_params and libpq APIs. + */ + char nodeIds[NODE_ARRAY_MAX_COUNT][NODEID_MAX_LENGTH]; + char lsns[NODE_ARRAY_MAX_COUNT][PG_LSN_MAXLENGTH]; +} nodesArraysValuesParams; + + +static bool +BuildNodesArrayValues(NodeAddressArray *nodeArray, + nodesArraysValuesParams *sqlParams, + PQExpBuffer values) +{ + int nodeIndex = 0; + int paramIndex = 0; + + /* when we didn't find any node to process, return our empty set */ + if (nodeArray->count == 0) + { + appendPQExpBufferStr( + values, + "SELECT id, lsn " + "FROM (values (null::int, null::pg_lsn)) as t(id, lsn) " + "where false"); + + return true; + } + + /* we start the VALUES subquery with the values SQL keyword */ + appendPQExpBufferStr(values, "values "); + + /* + * Build a SQL VALUES statement for every other node registered in the + * system, so that we can maintain their LSN position locally on a standby + * server. + */ + for (nodeIndex = 0; nodeIndex < nodeArray->count; nodeIndex++) + { + NodeAddress *node = &(nodeArray->nodes[nodeIndex]); + IntString nodeIdStr = intToString(node->nodeId); + char *nodeIdString = nodeIdStr.strValue; + + int idParamIndex = paramIndex; + int lsnParamIndex = paramIndex + 1; + + sqlParams->types[idParamIndex] = INT8OID; + strlcpy(sqlParams->nodeIds[nodeIndex], nodeIdString, NODEID_MAX_LENGTH); + + /* store the (char *) pointer to the data in values */ + sqlParams->values[idParamIndex] = sqlParams->nodeIds[nodeIndex]; + + sqlParams->types[lsnParamIndex] = LSNOID; + strlcpy(sqlParams->lsns[nodeIndex], node->lsn, PG_LSN_MAXLENGTH); + + /* store the (char *) pointer to the data in values */ + sqlParams->values[lsnParamIndex] = sqlParams->lsns[nodeIndex]; + + appendPQExpBuffer(values, + "%s($%d, $%d%s)", + paramIndex == 0 ? "" : ",", + + /* we begin at $1 here: intentional off-by-one */ + idParamIndex + 1, lsnParamIndex + 1, + + /* cast only the first row */ + paramIndex == 0 ? "::pg_lsn" : ""); + + /* prepare next round */ + paramIndex += 2; + } + + /* count how many parameters where appended to the VALUES() parts */ + sqlParams->count = paramIndex; + + return true; +} + + +/* + * pgsql_replication_slot_create_and_drop drops replication slots that belong + * to nodes that have been removed, and creates replication slots for nodes + * that have been newly registered. We call that function on the primary, where + * the slots are maintained by the replication protocol. + * + * On the standby nodes, we advance the slots ourselves and use the other + * function pgsql_replication_slot_maintain which is complete (create, drop, + * advance). + */ +bool +pgsql_replication_slot_create_and_drop(PGSQL *pgsql, NodeAddressArray *nodeArray) +{ + PQExpBuffer query = createPQExpBuffer(); + PQExpBuffer values = createPQExpBuffer(); + + /* *INDENT-OFF* */ + char *sqlTemplate = + /* + * We could simplify the writing of this query, but we prefer that it + * looks as much as possible like the query used in + * pgsql_replication_slot_maintain() so that we can maintain both + * easily. + */ + "WITH nodes(slot_name, lsn) as (" + " SELECT '" REPLICATION_SLOT_NAME_DEFAULT "_' || id, lsn" + " FROM (%s) as sb(id, lsn) " + "), \n" + "dropped as (" + " SELECT slot_name, pg_drop_replication_slot(slot_name) " + " FROM pg_replication_slots pgrs LEFT JOIN nodes USING(slot_name) " + " WHERE nodes.slot_name IS NULL " + " AND ( slot_name ~ '" REPLICATION_SLOT_NAME_PATTERN "' " + " OR slot_name ~ '" REPLICATION_SLOT_NAME_DEFAULT "' )" + " AND not active" + " AND slot_type = 'physical'" + "), \n" + "created as (" + "SELECT c.slot_name, c.lsn " + " FROM nodes LEFT JOIN pg_replication_slots pgrs USING(slot_name), " + " LATERAL pg_create_physical_replication_slot(slot_name, true) c" + " WHERE pgrs.slot_name IS NULL " + ") \n" + "SELECT 'create', slot_name, lsn FROM created " + " union all " + "SELECT 'drop', slot_name, NULL::pg_lsn FROM dropped"; + /* *INDENT-ON* */ + + nodesArraysValuesParams sqlParams = { 0 }; + ReplicationSlotMaintainContext context = { 0 }; + + if (!BuildNodesArrayValues(nodeArray, &sqlParams, values)) + { + /* errors have already been logged */ + destroyPQExpBuffer(query); + destroyPQExpBuffer(values); + + return false; + } + + /* add the computed ($1,$2), ... string to the query "template" */ + appendPQExpBuffer(query, sqlTemplate, values->data); + + bool success = + pgsql_execute_with_params(pgsql, + query->data, + sqlParams.count, + sqlParams.types, + (const char **) sqlParams.values, + &context, + parseReplicationSlotMaintain); + + destroyPQExpBuffer(query); + destroyPQExpBuffer(values); + + return success; +} + + +/* + * pgsql_replication_slot_maintain creates, drops, and advance replication + * slots that belong to other standby nodes. We call that function on the + * standby nodes, where the slots are maintained manually just in case we need + * them at failover. + */ +bool +pgsql_replication_slot_maintain(PGSQL *pgsql, NodeAddressArray *nodeArray) +{ + PQExpBuffer query = createPQExpBuffer(); + PQExpBuffer values = createPQExpBuffer(); + + /* *INDENT-OFF* */ + char *sqlTemplate = + "WITH nodes(slot_name, lsn) as (" + " SELECT '" REPLICATION_SLOT_NAME_DEFAULT "_' || id, lsn" + " FROM (%s) as sb(id, lsn) " + "), \n" + "dropped as (" + " SELECT slot_name, pg_drop_replication_slot(slot_name) " + " FROM pg_replication_slots pgrs LEFT JOIN nodes USING(slot_name) " + " WHERE nodes.slot_name IS NULL " + " AND slot_name ~ '" REPLICATION_SLOT_NAME_PATTERN "' " + " AND not active" + " AND slot_type = 'physical'" + "), \n" + "advanced as (" + "SELECT a.slot_name, a.end_lsn" + " FROM pg_replication_slots s JOIN nodes USING(slot_name), " + " LATERAL pg_replication_slot_advance(slot_name, lsn) a" + " WHERE nodes.lsn <> '0/0' and nodes.lsn >= s.restart_lsn " + " and not s.active " + "), \n" + "created as (" + "SELECT c.slot_name, c.lsn " + " FROM nodes LEFT JOIN pg_replication_slots pgrs USING(slot_name), " + " LATERAL pg_create_physical_replication_slot(slot_name, true) c" + " WHERE pgrs.slot_name IS NULL " + ") \n" + "SELECT 'create', slot_name, lsn FROM created " + " union all " + "SELECT 'drop', slot_name, NULL::pg_lsn FROM dropped " + " union all " + "SELECT 'advance', slot_name, end_lsn FROM advanced "; + /* *INDENT-ON* */ + + nodesArraysValuesParams sqlParams = { 0 }; + ReplicationSlotMaintainContext context = { 0 }; + + if (!BuildNodesArrayValues(nodeArray, &sqlParams, values)) + { + /* errors have already been logged */ + destroyPQExpBuffer(query); + destroyPQExpBuffer(values); + + return false; + } + + /* add the computed ($1,$2), ... string to the query "template" */ + appendPQExpBuffer(query, sqlTemplate, values->data); + + bool success = + pgsql_execute_with_params(pgsql, + query->data, + sqlParams.count, + sqlParams.types, + (const char **) sqlParams.values, + &context, + parseReplicationSlotMaintain); + + destroyPQExpBuffer(query); + destroyPQExpBuffer(values); + + return success; +} + + +/* + * parseReplicationSlotMaintain parses the result from a PostgreSQL query + * fetching two columns from pg_stat_replication: sync_state and currentLSN. + */ +static void +parseReplicationSlotMaintain(void *ctx, PGresult *result) +{ + int rowNumber = 0; + ReplicationSlotMaintainContext *context = + (ReplicationSlotMaintainContext *) ctx; + + if (PQnfields(result) != 3) + { + log_error("Query returned %d columns, expected 3", PQnfields(result)); + context->parsedOK = false; + return; + } + + for (rowNumber = 0; rowNumber < PQntuples(result); rowNumber++) + { + /* operation and slotName can't be NULL given how the SQL is built */ + char *operation = PQgetvalue(result, rowNumber, 0); + char *slotName = PQgetvalue(result, rowNumber, 1); + char *lsn = PQgetisnull(result, rowNumber, 2) ? "" + : PQgetvalue(result, rowNumber, 2); + + /* adding or removing another standby node is worthy of a log line */ + if (strcmp(operation, "create") == 0) + { + log_info("Creating replication slot \"%s\"", slotName); + } + else if (strcmp(operation, "drop") == 0) + { + log_info("Dropping replication slot \"%s\"", slotName); + } + else + { + log_debug("parseReplicationSlotMaintain: %s %s %s", + operation, slotName, lsn); + } + } + + context->parsedOK = true; +} + + +/* + * pgsql_disable_synchronous_replication disables synchronous replication + * in Postgres such that writes do not block if there is no replica. + */ +bool +pgsql_disable_synchronous_replication(PGSQL *pgsql) +{ + GUC setting = { "synchronous_standby_names", "''" }; + char *cancelBlockedStatementsCommand = + "SELECT pg_cancel_backend(pid) " + " FROM pg_stat_activity " + " WHERE wait_event = 'SyncRep'"; + + log_info("Disabling synchronous replication"); + + if (!pgsql_alter_system_set(pgsql, setting)) + { + return false; + } + + log_debug("Unblocking commands waiting for synchronous replication"); + + if (!pgsql_execute(pgsql, cancelBlockedStatementsCommand)) + { + return false; + } + + return true; +} + + +/* + * pgsql_set_default_transaction_mode_read_only makes it so that the server + * won't be a target of a connection string requiring target_session_attrs + * read-write by issuing ALTER SYSTEM SET transaction_mode_read_only TO on; + * + */ +bool +pgsql_set_default_transaction_mode_read_only(PGSQL *pgsql) +{ + GUC setting = { "default_transaction_read_only", "'on'" }; + + log_info("Setting default_transaction_read_only to on"); + + return pgsql_alter_system_set(pgsql, setting); +} + + +/* + * pgsql_set_default_transaction_mode_read_write makes it so that the server + * can be a target of a connection string requiring target_session_attrs + * read-write by issuing ALTER SYSTEM SET transaction_mode_read_only TO off; + * + */ +bool +pgsql_set_default_transaction_mode_read_write(PGSQL *pgsql) +{ + GUC setting = { "default_transaction_read_only", "'off'" }; + + log_info("Setting default_transaction_read_only to off"); + + return pgsql_alter_system_set(pgsql, setting); +} + + +/* + * pgsql_checkpoint runs a CHECKPOINT command on postgres to trigger a checkpoint. + */ +bool +pgsql_checkpoint(PGSQL *pgsql) +{ + return pgsql_execute(pgsql, "CHECKPOINT"); +} + + +/* + * pgsql_alter_system_set runs an ALTER SYSTEM SET ... command on Postgres + * to globally set a GUC and then runs pg_reload_conf() to make existing + * sessions reload it. + */ +static bool +pgsql_alter_system_set(PGSQL *pgsql, GUC setting) +{ + char command[BUFSIZE]; + + sformat(command, sizeof(command), + "ALTER SYSTEM SET %s TO %s", setting.name, setting.value); + + if (!pgsql_execute(pgsql, command)) + { + log_error("Failed to set \"%s\" to \"%s\" with ALTER SYSTEM, " + "see above for details", + setting.name, setting.value); + return false; + } + + if (!pgsql_reload_conf(pgsql)) + { + log_error("Failed to reload Postgres config after ALTER SYSTEM " + "to set \"%s\" to \"%s\".", + setting.name, setting.value); + return false; + } + + return true; +} + + +/* + * pgsql_reset_primary_conninfo issues the following SQL commands: + * + * ALTER SYSTEM RESET primary_conninfo; + * ALTER SYSTEM RESET primary_slot_name; + * + * That's necessary to clean-up the replication settings that pg_basebackup + * puts in place in postgresql.auto.conf in Postgres 12. We don't reload the + * configuration after the RESET in that case, because Postgres 12 requires a + * restart to apply the new setting value anyway. + */ +bool +pgsql_reset_primary_conninfo(PGSQL *pgsql) +{ + char *reset_primary_conninfo = "ALTER SYSTEM RESET primary_conninfo"; + char *reset_primary_slot_name = "ALTER SYSTEM RESET primary_slot_name"; + + /* ALTER SYSTEM cannot run inside a transaction block */ + if (!pgsql_execute(pgsql, reset_primary_conninfo)) + { + return false; + } + + if (!pgsql_execute(pgsql, reset_primary_slot_name)) + { + return false; + } + + return true; +} + + +/* + * pgsql_reload_conf causes open sessions to reload the PostgreSQL configuration + * files. + */ +bool +pgsql_reload_conf(PGSQL *pgsql) +{ + char *sql = "SELECT pg_reload_conf()"; + + log_info("Reloading Postgres configuration and HBA rules"); + + return pgsql_execute(pgsql, sql); +} + + +/* + * pgsql_get_hba_file_path gets the value of the hba_file setting in + * Postgres or returns false if a failure occurred. The value is copied to + * the hbaFilePath pointer. + */ +bool +pgsql_get_hba_file_path(PGSQL *pgsql, char *hbaFilePath, int maxPathLength) +{ + char *configValue = NULL; + + if (!pgsql_get_current_setting(pgsql, "hba_file", &configValue)) + { + /* pgsql_get_current_setting logs a relevant error */ + return false; + } + + int hbaFilePathLength = strlcpy(hbaFilePath, configValue, maxPathLength); + + if (hbaFilePathLength >= maxPathLength) + { + log_error("The hba_file \"%s\" returned by postgres is %d characters, " + "the maximum supported by pg_autoctl is %d characters", + configValue, hbaFilePathLength, maxPathLength); + free(configValue); + return false; + } + + free(configValue); + + return true; +} + + +/* + * pgsql_get_current_setting gets the value of a GUC in Postgres by running + * SELECT current_setting($settingName), or returns false if a failure occurred. + * + * If getting the value was successful, currentValue will point to a copy of the + * value which should be freed by the caller. + */ +static bool +pgsql_get_current_setting(PGSQL *pgsql, char *settingName, char **currentValue) +{ + SingleValueResultContext context = { 0 }; + char *sql = "SELECT current_setting($1)"; + int paramCount = 1; + Oid paramTypes[1] = { TEXTOID }; + const char *paramValues[1] = { settingName }; + + context.resultType = PGSQL_RESULT_STRING; + + if (!pgsql_execute_with_params(pgsql, sql, paramCount, paramTypes, paramValues, + &context, &parseSingleValueResult)) + { + /* errors have already been logged */ + return false; + } + + if (!context.parsedOk) + { + log_error("Failed to get result from current_setting('%s')", settingName); + return false; + } + + *currentValue = context.strVal; + + return true; +} + + +/* + * pgsql_create_database issues a CREATE DATABASE statement. + */ +bool +pgsql_create_database(PGSQL *pgsql, const char *dbname, const char *owner) +{ + char command[BUFSIZE]; + char *escapedDBName, *escapedOwner; + + /* open a connection upfront since it is needed by PQescape functions */ + PGconn *connection = pgsql_open_connection(pgsql); + if (connection == NULL) + { + /* error message was logged in pgsql_open_connection */ + return false; + } + + /* escape the dbname */ + escapedDBName = PQescapeIdentifier(connection, dbname, strlen(dbname)); + if (escapedDBName == NULL) + { + log_error("Failed to create database \"%s\": %s", dbname, + PQerrorMessage(connection)); + pgsql_finish(pgsql); + return false; + } + + /* escape the username */ + escapedOwner = PQescapeIdentifier(connection, owner, strlen(owner)); + if (escapedOwner == NULL) + { + log_error("Failed to create database \"%s\": %s", dbname, + PQerrorMessage(connection)); + PQfreemem(escapedDBName); + pgsql_finish(pgsql); + return false; + } + + /* now build the SQL command */ + sformat(command, BUFSIZE, + "CREATE DATABASE %s WITH OWNER %s", + escapedDBName, + escapedOwner); + + log_debug("Running command on Postgres: %s;", command); + + PQfreemem(escapedDBName); + PQfreemem(escapedOwner); + + PGresult *result = PQexec(connection, command); + + if (!is_response_ok(result)) + { + /* + * Check if we have a duplicate_database (42P04) error, in which case + * it means the user has already been created, accept that as a + * non-error, only inform about the situation. + */ + char *sqlstate = PQresultErrorField(result, PG_DIAG_SQLSTATE); + + if (strcmp(sqlstate, STR_ERRCODE_DUPLICATE_DATABASE) == 0) + { + log_info("The database \"%s\" already exists, skipping.", dbname); + } + else + { + log_error("Failed to create database \"%s\"[%s]: %s", + dbname, sqlstate, PQerrorMessage(connection)); + PQclear(result); + clear_results(pgsql); + pgsql_finish(pgsql); + return false; + } + } + + PQclear(result); + clear_results(pgsql); + if (pgsql->connectionStatementType == PGSQL_CONNECTION_SINGLE_STATEMENT) + { + pgsql_finish(pgsql); + } + + return true; +} + + +/* + * pgsql_create_extension issues a CREATE EXTENSION statement. + */ +bool +pgsql_create_extension(PGSQL *pgsql, const char *name) +{ + char command[BUFSIZE]; + + /* open a connection upfront since it is needed by PQescape functions */ + PGconn *connection = pgsql_open_connection(pgsql); + if (connection == NULL) + { + /* error message was logged in pgsql_open_connection */ + return false; + } + + /* escape the dbname */ + char *escapedIdentifier = PQescapeIdentifier(connection, name, strlen(name)); + if (escapedIdentifier == NULL) + { + log_error("Failed to create extension \"%s\": %s", name, + PQerrorMessage(connection)); + pgsql_finish(pgsql); + return false; + } + + /* now build the SQL command */ + sformat(command, BUFSIZE, "CREATE EXTENSION IF NOT EXISTS %s CASCADE", + escapedIdentifier); + PQfreemem(escapedIdentifier); + log_debug("Running command on Postgres: %s;", command); + + PGresult *result = PQexec(connection, command); + + if (!is_response_ok(result)) + { + /* + * Check if we have a duplicate_object (42710) error, in which case + * it means the user has already been created, accept that as a + * non-error, only inform about the situation. + */ + char *sqlstate = PQresultErrorField(result, PG_DIAG_SQLSTATE); + + log_error("Failed to create extension \"%s\"[%s]: %s", + name, sqlstate, PQerrorMessage(connection)); + PQclear(result); + clear_results(pgsql); + pgsql_finish(pgsql); + return false; + } + + PQclear(result); + clear_results(pgsql); + if (pgsql->connectionStatementType == PGSQL_CONNECTION_SINGLE_STATEMENT) + { + pgsql_finish(pgsql); + } + + return true; +} + + +/* + * pgsql_create_user creates a user with the given settings. + * + * Unlike most functions this function does opens a connection itself + * because it has some specific requirements around logging, error handling + * and escaping. + */ +bool +pgsql_create_user(PGSQL *pgsql, const char *userName, const char *password, + bool login, bool superuser, bool replication, int connlimit) +{ + /* open a connection upfront since it is needed by PQescape functions */ + PGconn *connection = pgsql_open_connection(pgsql); + if (connection == NULL) + { + /* error message was logged in pgsql_open_connection */ + return false; + } + + /* escape the username */ + PQExpBuffer query = createPQExpBuffer(); + char *escapedIdentifier = PQescapeIdentifier(connection, userName, strlen(userName)); + if (escapedIdentifier == NULL) + { + log_error("Failed to create user \"%s\": %s", userName, + PQerrorMessage(connection)); + pgsql_finish(pgsql); + return false; + } + + appendPQExpBuffer(query, "CREATE USER %s", escapedIdentifier); + PQfreemem(escapedIdentifier); + + if (login || superuser || replication || password) + { + appendPQExpBufferStr(query, " WITH"); + } + if (login) + { + appendPQExpBufferStr(query, " LOGIN"); + } + if (superuser) + { + appendPQExpBufferStr(query, " SUPERUSER"); + } + if (replication) + { + appendPQExpBufferStr(query, " REPLICATION"); + } + if (connlimit > -1) + { + appendPQExpBuffer(query, " CONNECTION LIMIT %d", connlimit); + } + if (password) + { + /* show the statement before we append the password */ + log_debug("%s PASSWORD '*****';", query->data); + + escapedIdentifier = PQescapeLiteral(connection, password, strlen(password)); + if (escapedIdentifier == NULL) + { + log_error("Failed to create user \"%s\": %s", userName, + PQerrorMessage(connection)); + PQfreemem(escapedIdentifier); + pgsql_finish(pgsql); + destroyPQExpBuffer(query); + return false; + } + + appendPQExpBuffer(query, " PASSWORD %s", escapedIdentifier); + PQfreemem(escapedIdentifier); + } + else + { + log_debug("%s;", query->data); + } + + /* memory allocation could have failed while building string */ + if (PQExpBufferBroken(query)) + { + log_error("Failed to allocate memory"); + destroyPQExpBuffer(query); + pgsql_finish(pgsql); + return false; + } + + /* + * Set the libpq notice receiver to integrate notifications as debug + * message, because when dealing with the citus extension those messages + * are not that interesting to our pg_autoctl users frankly: + * + * NOTICE: not propagating CREATE ROLE/USER commands to worker nodes + * HINT: Connect to worker nodes directly... + */ + PQnoticeProcessor previousNoticeProcessor = + PQsetNoticeProcessor(connection, &pgAutoCtlDebugNoticeProcessor, NULL); + + PGresult *result = PQexec(connection, query->data); + destroyPQExpBuffer(query); + + if (!is_response_ok(result)) + { + /* + * Check if we have a duplicate_object (42710) error, in which case + * it means the user has already been created, accept that as a + * non-error, only inform about the situation. + */ + char *sqlstate = PQresultErrorField(result, PG_DIAG_SQLSTATE); + + if (strcmp(sqlstate, STR_ERRCODE_DUPLICATE_OBJECT) == 0) + { + log_info("The user \"%s\" already exists, skipping.", userName); + } + else + { + log_error("Failed to create user \"%s\"[%s]: %s", + userName, sqlstate, PQerrorMessage(connection)); + PQclear(result); + clear_results(pgsql); + pgsql_finish(pgsql); + return false; + } + } + + PQclear(result); + clear_results(pgsql); + + if (pgsql->connectionStatementType == PGSQL_CONNECTION_SINGLE_STATEMENT) + { + pgsql_finish(pgsql); + } + else + { + /* restore the normal notice message processing, if needed. */ + PQsetNoticeProcessor(connection, previousNoticeProcessor, NULL); + } + + return true; +} + + +/* + * pgsql_alter_role_password runs ALTER ROLE PASSWORD '...'. + * + * The password is never logged; we log "ALTER ROLE PASSWORD '*****'" + * instead. Uses PQescapeIdentifier / PQescapeLiteral so the values are safe + * to interpolate directly into the query string. + */ +bool +pgsql_alter_role_password(PGSQL *pgsql, const char *roleName, + const char *password) +{ + /* open a connection upfront since it is needed by PQescape functions */ + PGconn *connection = pgsql_open_connection(pgsql); + if (connection == NULL) + { + /* error message was logged in pgsql_open_connection */ + return false; + } + + char *escapedRole = PQescapeIdentifier(connection, roleName, strlen(roleName)); + if (escapedRole == NULL) + { + log_error("Failed to escape role name \"%s\": %s", + roleName, PQerrorMessage(connection)); + pgsql_finish(pgsql); + return false; + } + + char *escapedPassword = PQescapeLiteral(connection, password, strlen(password)); + if (escapedPassword == NULL) + { + log_error("Failed to escape password for role \"%s\": %s", + roleName, PQerrorMessage(connection)); + PQfreemem(escapedRole); + pgsql_finish(pgsql); + return false; + } + + /* log without the real password */ + log_debug("ALTER ROLE %s PASSWORD '*****';", escapedRole); + + PQExpBuffer query = createPQExpBuffer(); + appendPQExpBuffer(query, "ALTER ROLE %s PASSWORD %s", escapedRole, escapedPassword); + PQfreemem(escapedRole); + PQfreemem(escapedPassword); + + if (PQExpBufferBroken(query)) + { + log_error("Failed to allocate memory"); + destroyPQExpBuffer(query); + pgsql_finish(pgsql); + return false; + } + + PGresult *result = PQexec(connection, query->data); + destroyPQExpBuffer(query); + + if (!is_response_ok(result)) + { + log_error("Failed to alter role \"%s\" password: %s", + roleName, PQerrorMessage(connection)); + PQclear(result); + clear_results(pgsql); + pgsql_finish(pgsql); + return false; + } + + PQclear(result); + clear_results(pgsql); + + if (pgsql->connectionStatementType == PGSQL_CONNECTION_SINGLE_STATEMENT) + { + pgsql_finish(pgsql); + } + + return true; +} + + +/* + * pgsql_has_replica returns whether a replica with the given username is active. + */ +bool +pgsql_has_replica(PGSQL *pgsql, char *userName, bool *hasReplica) +{ + SingleValueResultContext context = { { 0 }, PGSQL_RESULT_BOOL, false }; + + /* + * Check whether there is an entry in pg_stat_replication, which means + * there is either a pg_basebackup or streaming replica active. In either + * case, it means there is a replica that recently communicated with the + * postgres server, which is all we care about for the purpose of this + * function. + */ + char *sql = + "SELECT EXISTS (SELECT 1 FROM pg_stat_replication WHERE usename = $1)"; + + const Oid paramTypes[1] = { TEXTOID }; + const char *paramValues[1] = { userName }; + int paramCount = 1; + + if (!pgsql_execute_with_params(pgsql, sql, paramCount, paramTypes, paramValues, + &context, &parseSingleValueResult)) + { + /* errors have already been logged */ + return false; + } + + if (!context.parsedOk) + { + log_error("Failed to find pg_stat_replication"); + return false; + } + + *hasReplica = context.boolVal; + + return true; +} + + +/* + * hostname_from_uri parses a PostgreSQL connection string URI and returns + * whether the URL was successfully parsed. + */ +bool +hostname_from_uri(const char *pguri, + char *hostname, int maxHostLength, int *port) +{ + int found = 0; + char *errmsg; + PQconninfoOption *conninfo, *option; + + conninfo = PQconninfoParse(pguri, &errmsg); + if (conninfo == NULL) + { + log_error("Failed to parse pguri \"%s\": %s", pguri, errmsg); + PQfreemem(errmsg); + return false; + } + + for (option = conninfo; option->keyword != NULL; option++) + { + if (strcmp(option->keyword, "host") == 0 || + strcmp(option->keyword, "hostaddr") == 0) + { + if (option->val) + { + int hostNameLength = strlcpy(hostname, option->val, maxHostLength); + + if (hostNameLength >= maxHostLength) + { + log_error( + "The URL \"%s\" contains a hostname of %d characters, " + "the maximum supported by pg_autoctl is %d characters", + option->val, hostNameLength, maxHostLength); + PQconninfoFree(conninfo); + return false; + } + + ++found; + } + } + + if (strcmp(option->keyword, "port") == 0) + { + if (option->val) + { + /* we expect a single port number in a monitor's URI */ + if (!stringToInt(option->val, port)) + { + log_error("Failed to parse port number : %s", option->val); + + PQconninfoFree(conninfo); + return false; + } + ++found; + } + else + { + *port = POSTGRES_PORT; + } + } + + if (found == 2) + { + break; + } + } + PQconninfoFree(conninfo); + + return true; +} + + +/* + * validate_connection_string takes a connection string and parses it with + * libpq, varifying that it's well formed and usable. + */ +bool +validate_connection_string(const char *connectionString) +{ + char *errorMessage = NULL; + + int length = strlen(connectionString); + if (length >= MAXCONNINFO) + { + log_error("Connection string \"%s\" is %d " + "characters, the maximum supported by pg_autoctl is %d", + connectionString, length, MAXCONNINFO); + return false; + } + + PQconninfoOption *connInfo = PQconninfoParse(connectionString, &errorMessage); + if (connInfo == NULL) + { + log_error("Failed to parse connection string \"%s\": %s ", + connectionString, errorMessage); + PQfreemem(errorMessage); + return false; + } + + PQconninfoFree(connInfo); + + return true; +} + + +/* + * pgsql_get_postgres_metadata returns several bits of information that we need + * to take decisions in the rest of the code: + * + * - pg_is_in_recovery (primary or standby, as expected?) + * - sync_state from pg_stat_replication when a primary + * - current_lsn from the server + * - pg_control_version + * - catalog_version_no + * - system_identifier + * + * With those metadata we can then check our expectations and take decisions in + * some cases. We can obtain all the metadata that we need easily enough in a + * single SQL query, so that's what we do. + */ +typedef struct PgMetadata +{ + char sqlstate[6]; + bool parsedOk; + bool pg_is_in_recovery; + char syncState[PGSR_SYNC_STATE_MAXLENGTH]; + char currentLSN[PG_LSN_MAXLENGTH]; + PostgresControlData control; +} PgMetadata; + + +bool +pgsql_get_postgres_metadata(PGSQL *pgsql, + bool *pg_is_in_recovery, + char *pgsrSyncState, + char *currentLSN, + PostgresControlData *control) +{ + PgMetadata context = { 0 }; + + /* *INDENT-OFF* */ + char *sql = + /* + * Make it so that we still have the current WAL LSN even in the case + * where there's no replication slot in use by any standby. + * + * When on the primary, we might have multiple standby nodes connected. + * We're good when at least one of them is either 'sync' or 'quorum'. + * We don't check individual replication slots, we take the "best" one + * and report that. + */ + "select pg_is_in_recovery()," + " coalesce(rep.sync_state, '') as sync_state," + " case when pg_is_in_recovery()" + " then coalesce(pg_last_wal_receive_lsn(), pg_last_wal_replay_lsn())" + " else pg_current_wal_flush_lsn()" + " end as current_lsn," + " pg_control_version, catalog_version_no, system_identifier," + " case when pg_is_in_recovery()" + " then (select received_tli from pg_stat_wal_receiver)" + " else (select timeline_id from pg_control_checkpoint()) " + " end as timeline_id " + " from (values(1)) as dummy" + " full outer join" + " (select pg_control_version, catalog_version_no, system_identifier " + " from pg_control_system()" + " )" + " as control on true" + " full outer join" + " (" + " select sync_state" + " from pg_replication_slots slot" + " join pg_stat_replication rep" + " on rep.pid = slot.active_pid" + " where slot_name ~ '" REPLICATION_SLOT_NAME_PATTERN "' " + " or slot_name = '" REPLICATION_SLOT_NAME_DEFAULT "' " + "order by case sync_state " + " when 'quorum' then 4 " + " when 'sync' then 3 " + " when 'potential' then 2 " + " when 'async' then 1 " + " else 0 end " + " desc limit 1" + " ) " + "as rep on true"; + /* *INDENT-ON* */ + + if (!pgsql_execute_with_params(pgsql, sql, 0, NULL, NULL, + &context, &parsePgMetadata)) + { + /* errors have been logged already */ + return false; + } + + if (!context.parsedOk) + { + log_error("Failed to parse the Postgres metadata"); + return false; + } + + *pg_is_in_recovery = context.pg_is_in_recovery; + + /* the last two metadata items are opt-in */ + if (pgsrSyncState != NULL) + { + strlcpy(pgsrSyncState, context.syncState, PGSR_SYNC_STATE_MAXLENGTH); + } + + if (currentLSN != NULL) + { + strlcpy(currentLSN, context.currentLSN, PG_LSN_MAXLENGTH); + } + + /* overwrite the Control Data fetched from the query */ + *control = context.control; + + pgsql_finish(pgsql); + + return true; +} + + +/* + * parsePgMetadata parses the result from a PostgreSQL query fetching + * two columns from pg_stat_replication: sync_state and currentLSN. + */ +static void +parsePgMetadata(void *ctx, PGresult *result) +{ + PgMetadata *context = (PgMetadata *) ctx; + char *value; + + if (PQnfields(result) != 7) + { + log_error("Query returned %d columns, expected 7", PQnfields(result)); + context->parsedOk = false; + return; + } + + if (PQntuples(result) != 1) + { + log_error("Query returned %d rows, expected 1", PQntuples(result)); + context->parsedOk = false; + return; + } + + context->pg_is_in_recovery = strcmp(PQgetvalue(result, 0, 0), "t") == 0; + + if (!PQgetisnull(result, 0, 1)) + { + value = PQgetvalue(result, 0, 1); + + strlcpy(context->syncState, value, PGSR_SYNC_STATE_MAXLENGTH); + } + else + { + context->syncState[0] = '\0'; + } + + if (!PQgetisnull(result, 0, 2)) + { + value = PQgetvalue(result, 0, 2); + + strlcpy(context->currentLSN, value, PG_LSN_MAXLENGTH); + } + else + { + context->currentLSN[0] = '\0'; + } + + value = PQgetvalue(result, 0, 3); + if (!stringToUInt(value, &(context->control.pg_control_version))) + { + log_error("Failed to parse pg_control_version \"%s\"", value); + context->parsedOk = true; + return; + } + + value = PQgetvalue(result, 0, 4); + if (!stringToUInt(value, &(context->control.catalog_version_no))) + { + log_error("Failed to parse catalog_version_no \"%s\"", value); + context->parsedOk = true; + return; + } + + value = PQgetvalue(result, 0, 5); + if (!stringToUInt64(value, &(context->control.system_identifier))) + { + log_error("Failed to parse system_identifier \"%s\"", value); + context->parsedOk = true; + return; + } + + /* + * On a standby node that doesn't have a primary_conninfo then we fail to + * retrieve the received_tli from pg_stat_wal_receiver. We encode the NULL + * we get in that case with a zero, which is not a value we expect. + */ + if (PQgetisnull(result, 0, 6)) + { + context->control.timeline_id = 0; + } + else + { + value = PQgetvalue(result, 0, 6); + if (!stringToUInt(value, &(context->control.timeline_id))) + { + log_error("Failed to parse timeline_id \"%s\"", value); + context->parsedOk = true; + return; + } + } + + context->parsedOk = true; +} + + +typedef struct PgReachedTargetLSN +{ + char sqlstate[6]; + bool parsedOk; + bool hasReachedLSN; + char currentLSN[PG_LSN_MAXLENGTH]; + bool noRows; +} PgReachedTargetLSN; + + +/* + * pgsql_one_slot_has_reached_target_lsn checks that at least one replication + * slot has reached the given LSN already, using the Postgres system views + * pg_replication_slots and pg_stat_replication on the primary server. + */ +bool +pgsql_one_slot_has_reached_target_lsn(PGSQL *pgsql, + char *targetLSN, + char *currentLSN, + bool *hasReachedLSN) +{ + PgReachedTargetLSN context = { 0 }; + + /* + * We pick the most advanced LSN reached by the pgautofailover replication + * slots, and only consider those that have made it to "sync" or "quorum" + * sync_state already. This function is typically called after sync rep has + * been enabled on the primary. + */ + + /* *INDENT-OFF* */ + char *sql = + " select $1::pg_lsn <= flush_lsn, flush_lsn " + " from pg_replication_slots slot" + " join pg_stat_replication rep" + " on rep.pid = slot.active_pid" + " where ( slot_name ~ '" REPLICATION_SLOT_NAME_PATTERN "' " + " or slot_name = '" REPLICATION_SLOT_NAME_DEFAULT "') " + " and sync_state in ('sync', 'quorum') " + "order by flush_lsn desc limit 1"; + /* *INDENT-ON* */ + + const Oid paramTypes[1] = { LSNOID }; + const char *paramValues[1] = { targetLSN }; + + if (!pgsql_execute_with_params(pgsql, sql, 1, paramTypes, paramValues, + &context, &parsePgReachedTargetLSN)) + { + /* errors have been logged already */ + return false; + } + + if (!context.parsedOk) + { + if (context.noRows) + { + log_warn("No standby nodes are connected at the moment"); + } + else + { + log_error("Failed to fetch current flush_lsn location for " + "connected standby nodes, see above for details"); + } + return false; + } + + *hasReachedLSN = context.hasReachedLSN; + strlcpy(currentLSN, context.currentLSN, PG_LSN_MAXLENGTH); + + return true; +} + + +/* + * pgsql_has_reached_target_lsn calls pg_last_wal_replay_lsn() and compares the + * current LSN on the system to the given targetLSN. + */ +bool +pgsql_has_reached_target_lsn(PGSQL *pgsql, char *targetLSN, + char *currentLSN, bool *hasReachedLSN) +{ + PgReachedTargetLSN context = { 0 }; + char *sql = + "SELECT $1::pg_lsn <= pg_last_wal_replay_lsn(), " + " pg_last_wal_replay_lsn()"; + + const Oid paramTypes[1] = { LSNOID }; + const char *paramValues[1] = { targetLSN }; + + if (!pgsql_execute_with_params(pgsql, sql, 1, paramTypes, paramValues, + &context, &parsePgReachedTargetLSN)) + { + /* errors have been logged already */ + return false; + } + + if (!context.parsedOk) + { + log_error("Failed to get result from pg_last_wal_replay_lsn()"); + return false; + } + + *hasReachedLSN = context.hasReachedLSN; + strlcpy(currentLSN, context.currentLSN, PG_LSN_MAXLENGTH); + + return true; +} + + +/* + * parsePgMetadata parses the result from a PostgreSQL query fetching + * two columns from pg_stat_replication: sync_state and currentLSN. + */ +static void +parsePgReachedTargetLSN(void *ctx, PGresult *result) +{ + PgReachedTargetLSN *context = (PgReachedTargetLSN *) ctx; + + if (PQnfields(result) != 2) + { + log_error("Query returned %d columns, expected 2", PQnfields(result)); + context->parsedOk = false; + return; + } + + if (PQntuples(result) == 0) + { + log_debug("parsePgReachedTargetLSN: query returned no rows"); + context->parsedOk = false; + context->noRows = true; + return; + } + if (PQntuples(result) != 1) + { + log_error("Query returned %d rows, expected 1", PQntuples(result)); + context->parsedOk = false; + return; + } + + context->hasReachedLSN = strcmp(PQgetvalue(result, 0, 0), "t") == 0; + + if (!PQgetisnull(result, 0, 1)) + { + char *value = PQgetvalue(result, 0, 1); + + strlcpy(context->currentLSN, value, PG_LSN_MAXLENGTH); + } + else + { + context->currentLSN[0] = '\0'; + } + + context->parsedOk = true; +} + + +typedef struct IdentifySystemResult +{ + char sqlstate[6]; + bool parsedOk; + IdentifySystem *system; +} IdentifySystemResult; + + +typedef struct TimelineHistoryResult +{ + char sqlstate[6]; + bool parsedOk; + char filename[MAXPGPATH]; + char content[BUFSIZE * BUFSIZE]; /* 1MB should get us quite very far */ +} TimelineHistoryResult; + + +/* + * pgsql_identify_system connects to the given pgsql client and issue the + * replication command IDENTIFY_SYSTEM. The pgsql connection string should + * contain the 'replication=1' parameter. + */ +bool +pgsql_identify_system(PGSQL *pgsql, IdentifySystem *system) +{ + PGconn *connection = pgsql_open_connection(pgsql); + if (connection == NULL) + { + /* error message was logged in pgsql_open_connection */ + return false; + } + + /* extended query protocol not supported in a replication connection */ + PGresult *result = PQexec(connection, "IDENTIFY_SYSTEM"); + + if (!is_response_ok(result)) + { + log_error("Failed to IDENTIFY_SYSTEM: %s", PQerrorMessage(connection)); + PQclear(result); + clear_results(pgsql); + + PQfinish(connection); + + return false; + } + + IdentifySystemResult isContext = { { 0 }, false, system }; + + (void) parseIdentifySystemResult((void *) &isContext, result); + + PQclear(result); + clear_results(pgsql); + + log_debug("IDENTIFY_SYSTEM: timeline %d, xlogpos %s, systemid %" PRIu64, + system->timeline, + system->xlogpos, + system->identifier); + + if (!isContext.parsedOk) + { + log_error("Failed to get result from IDENTIFY_SYSTEM"); + PQfinish(connection); + return false; + } + + /* while at it, we also run the TIMELINE_HISTORY command */ + if (system->timeline > 1) + { + TimelineHistoryResult hContext = { 0 }; + + char sql[BUFSIZE] = { 0 }; + sformat(sql, sizeof(sql), "TIMELINE_HISTORY %d", system->timeline); + + result = PQexec(connection, sql); + + if (!is_response_ok(result)) + { + log_error("Failed to request TIMELINE_HISTORY: %s", + PQerrorMessage(connection)); + PQclear(result); + clear_results(pgsql); + + PQfinish(connection); + + return false; + } + + (void) parseTimelineHistoryResult((void *) &hContext, result); + + PQclear(result); + clear_results(pgsql); + + if (!hContext.parsedOk) + { + log_error("Failed to get result from TIMELINE_HISTORY"); + PQfinish(connection); + return false; + } + + if (!parseTimeLineHistory(hContext.filename, hContext.content, system)) + { + /* errors have already been logged */ + PQfinish(connection); + return false; + } + + TimeLineHistoryEntry *current = + &(system->timelines.history[system->timelines.count - 1]); + + log_debug("TIMELINE_HISTORY: \"%s\", timeline %d started at %X/%X", + hContext.filename, + current->tli, + (uint32_t) (current->begin >> 32), + (uint32_t) current->begin); + } + + /* now we're done with running SQL queries */ + PQfinish(connection); + + return true; +} + + +/* + * parsePgMetadata parses the result from a PostgreSQL query fetching + * two columns from pg_stat_replication: sync_state and currentLSN. + */ +static void +parseIdentifySystemResult(void *ctx, PGresult *result) +{ + IdentifySystemResult *context = (IdentifySystemResult *) ctx; + + if (PQnfields(result) != 4) + { + log_error("Query returned %d columns, expected 4", PQnfields(result)); + context->parsedOk = false; + return; + } + + if (PQntuples(result) == 0) + { + log_debug("parseIdentifySystem: query returned no rows"); + context->parsedOk = false; + return; + } + if (PQntuples(result) != 1) + { + log_error("Query returned %d rows, expected 1", PQntuples(result)); + context->parsedOk = false; + return; + } + + /* systemid (text) */ + char *value = PQgetvalue(result, 0, 0); + if (!stringToUInt64(value, &(context->system->identifier))) + { + log_error("Failed to parse system_identifier \"%s\"", value); + context->parsedOk = false; + return; + } + + /* timeline (int4) */ + value = PQgetvalue(result, 0, 1); + if (!stringToUInt32(value, &(context->system->timeline))) + { + log_error("Failed to parse timeline \"%s\"", value); + context->parsedOk = false; + return; + } + + /* xlogpos (text) */ + value = PQgetvalue(result, 0, 2); + strlcpy(context->system->xlogpos, value, PG_LSN_MAXLENGTH); + + /* dbname (text) Database connected to or null */ + if (!PQgetisnull(result, 0, 3)) + { + value = PQgetvalue(result, 0, 3); + strlcpy(context->system->dbname, value, NAMEDATALEN); + } + + context->parsedOk = true; +} + + +/* + * parseTimelineHistory parses the result of the TIMELINE_HISTORY replication + * command. + */ +static void +parseTimelineHistoryResult(void *ctx, PGresult *result) +{ + TimelineHistoryResult *context = (TimelineHistoryResult *) ctx; + + if (PQnfields(result) != 2) + { + log_error("Query returned %d columns, expected 2", PQnfields(result)); + context->parsedOk = false; + return; + } + + if (PQntuples(result) == 0) + { + log_debug("parseTimelineHistory: query returned no rows"); + context->parsedOk = false; + return; + } + + if (PQntuples(result) != 1) + { + log_error("Query returned %d rows, expected 1", PQntuples(result)); + context->parsedOk = false; + return; + } + + /* filename (text) */ + char *value = PQgetvalue(result, 0, 0); + strlcpy(context->filename, value, sizeof(context->filename)); + + /* content (bytea) */ + value = PQgetvalue(result, 0, 1); + + if (strlen(value) >= sizeof(context->content)) + { + log_error("Received a timeline history file of %lu bytes, " + "pg_autoctl is limited to files of up to %lu bytes.", + (unsigned long) strlen(value), + (unsigned long) sizeof(context->content)); + context->parsedOk = false; + } + strlcpy(context->content, value, sizeof(context->content)); + + context->parsedOk = true; +} + + +/* + * parseTimeLineHistory parses the content of a timeline history file. + */ +bool +parseTimeLineHistory(const char *filename, const char *content, + IdentifySystem *system) +{ + int lineCount = countLines((char *) content); + char **historyLines = (char **) calloc(lineCount, sizeof(char *)); + int lineNumber = 0; + + if (historyLines == NULL) + { + log_error(ALLOCATION_FAILED_ERROR); + return false; + } + + splitLines((char *) content, historyLines, lineCount); + + uint64_t prevend = InvalidXLogRecPtr; + + system->timelines.count = 0; + + TimeLineHistoryEntry *entry = + &(system->timelines.history[system->timelines.count]); + + for (lineNumber = 0; lineNumber < lineCount; lineNumber++) + { + char *ptr = historyLines[lineNumber]; + + /* skip leading whitespace and check for # comment */ + for (; *ptr; ptr++) + { + if (!isspace((unsigned char) *ptr)) + { + break; + } + } + + if (*ptr == '\0' || *ptr == '#') + { + continue; + } + + log_trace("parseTimeLineHistory line %d is \"%s\"", + lineNumber, + historyLines[lineNumber]); + + char *tabptr = strchr(historyLines[lineNumber], '\t'); + + if (tabptr == NULL) + { + log_error("Failed to parse history file line %d: \"%s\"", + lineNumber, ptr); + free(historyLines); + return false; + } + + *tabptr = '\0'; + + if (!stringToUInt(historyLines[lineNumber], &(entry->tli))) + { + log_error("Failed to parse history timeline \"%s\"", tabptr); + free(historyLines); + return false; + } + + char *lsn = tabptr + 1; + + for (char *lsnend = lsn; *lsnend; lsnend++) + { + if (!(isxdigit((unsigned char) *lsnend) || *lsnend == '/')) + { + *lsnend = '\0'; + break; + } + } + + if (!parseLSN(lsn, &(entry->end))) + { + log_error("Failed to parse history timeline %d LSN \"%s\"", + entry->tli, lsn); + free(historyLines); + return false; + } + + entry->begin = prevend; + prevend = entry->end; + + log_trace("parseTimeLineHistory[%d]: tli %d [%X/%X %X/%X]", + system->timelines.count, + entry->tli, + (uint32) (entry->begin >> 32), + (uint32) entry->begin, + (uint32) (entry->end >> 32), + (uint32) entry->end); + + entry = &(system->timelines.history[++system->timelines.count]); + } + + free(historyLines); + + /* + * Create one more entry for the "tip" of the timeline, which has no entry + * in the history file. + */ + entry->tli = system->timeline; + entry->begin = prevend; + entry->end = InvalidXLogRecPtr; + + log_trace("parseTimeLineHistory[%d]: tli %d [%X/%X %X/%X]", + system->timelines.count, + entry->tli, + (uint32) (entry->begin >> 32), + (uint32) entry->begin, + (uint32) (entry->end >> 32), + (uint32) entry->end); + + /* fix the off-by-one so that the count is a count, not an index */ + ++system->timelines.count; + + return true; +} + + +/* + * LISTEN/NOTIFY support. + * + * First, send a LISTEN command. + */ +bool +pgsql_listen(PGSQL *pgsql, char *channels[]) +{ + PGresult *result = NULL; + char sql[BUFSIZE]; + + /* + * mark the connection as multi statement since it is going to be used by + * for processing notifications + */ + pgsql->connectionStatementType = PGSQL_CONNECTION_MULTI_STATEMENT; + + /* open a connection upfront since it is needed by PQescape functions */ + PGconn *connection = pgsql_open_connection(pgsql); + if (connection == NULL) + { + /* error message was logged in pgsql_open_connection */ + return false; + } + + for (int i = 0; channels[i]; i++) + { + char *channel = + PQescapeIdentifier(connection, channels[i], strlen(channels[i])); + + if (channel == NULL) + { + log_error("Failed to LISTEN \"%s\": %s", + channels[i], PQerrorMessage(connection)); + pgsql_finish(pgsql); + return false; + } + + sformat(sql, BUFSIZE, "LISTEN %s", channel); + + PQfreemem(channel); + + result = PQexec(connection, sql); + + if (!is_response_ok(result)) + { + log_error("Failed to LISTEN \"%s\": %s", + channels[i], PQerrorMessage(connection)); + PQclear(result); + clear_results(pgsql); + + return false; + } + + PQclear(result); + clear_results(pgsql); + } + + return true; +} + + +/* + * Preapre a multi statement connection which can later be used in wait for + * notification functions. + * + * Contrarry to pgsql_listen, this function, only prepares the connection and it + * is the user's responsibility to define which channels to listen to. + */ +bool +pgsql_prepare_to_wait(PGSQL *pgsql) +{ + /* + * mark the connection as multi statement since it is going to be used by + * for processing notifications + */ + pgsql->connectionStatementType = PGSQL_CONNECTION_MULTI_STATEMENT; + + /* open a connection upfront since it is needed by PQescape functions */ + PGconn *connection = pgsql_open_connection(pgsql); + if (connection == NULL) + { + /* error message was logged in pgsql_open_connection */ + return false; + } + + return true; +} + + +/* + * pgsql_alter_extension_update_to executes ALTER EXTENSION ... UPDATE TO ... + */ +bool +pgsql_alter_extension_update_to(PGSQL *pgsql, + const char *extname, const char *version) +{ + char command[BUFSIZE]; + char *escapedIdentifier, *escapedVersion; + + /* open a connection upfront since it is needed by PQescape functions */ + PGconn *connection = pgsql_open_connection(pgsql); + if (connection == NULL) + { + /* error message was logged in pgsql_open_connection */ + return false; + } + + /* escape the extname */ + escapedIdentifier = PQescapeIdentifier(connection, extname, strlen(extname)); + if (escapedIdentifier == NULL) + { + log_error("Failed to update extension \"%s\": %s", extname, + PQerrorMessage(connection)); + pgsql_finish(pgsql); + return false; + } + + /* escape the version */ + escapedVersion = PQescapeIdentifier(connection, version, strlen(version)); + if (escapedIdentifier == NULL) + { + log_error("Failed to update extension \"%s\" to version \"%s\": %s", + extname, version, + PQerrorMessage(connection)); + pgsql_finish(pgsql); + return false; + } + + /* now build the SQL command */ + int n = sformat(command, BUFSIZE, "ALTER EXTENSION %s UPDATE TO %s", + escapedIdentifier, escapedVersion); + + if (n >= BUFSIZE) + { + log_error("BUG: pg_autoctl only supports SQL string up to %d bytes, " + "a SQL string of %d bytes is needed to " + "update the \"%s\" extension.", + BUFSIZE, n, extname); + } + + PQfreemem(escapedIdentifier); + PQfreemem(escapedVersion); + + log_debug("Running command on Postgres: %s;", command); + + PGresult *result = PQexec(connection, command); + + if (!is_response_ok(result)) + { + char *sqlstate = PQresultErrorField(result, PG_DIAG_SQLSTATE); + + log_error("Error %s while running Postgres query: %s:", + sqlstate, command); + + char *message = PQerrorMessage(connection); + char *errorLines[BUFSIZE]; + int lineCount = splitLines(message, errorLines, BUFSIZE); + int lineNumber = 0; + + for (lineNumber = 0; lineNumber < lineCount; lineNumber++) + { + log_error("%s", errorLines[lineNumber]); + } + + PQclear(result); + clear_results(pgsql); + pgsql_finish(pgsql); + return false; + } + + PQclear(result); + clear_results(pgsql); + + return true; +} diff --git a/src/bin/common/pgsql.h b/src/bin/common/pgsql.h new file mode 100644 index 000000000..978f6b172 --- /dev/null +++ b/src/bin/common/pgsql.h @@ -0,0 +1,399 @@ +/* + * src/bin/pg_autoctl/pgsql.h + * Functions for interacting with a postgres server + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the PostgreSQL License. + * + */ + +#ifndef PGSQL_H +#define PGSQL_H + + +#include +#include + +#include "postgres.h" +#include "libpq-fe.h" +#include "portability/instr_time.h" + +#if PG_MAJORVERSION_NUM >= 15 +#include "common/pg_prng.h" +#endif + +#include "defaults.h" +#include "pgsetup.h" +#include "state.h" + + +/* + * OID values from PostgreSQL src/include/catalog/pg_type.h + */ +#define BOOLOID 16 +#define NAMEOID 19 +#define INT4OID 23 +#define INT8OID 20 +#define TEXTOID 25 +#define LSNOID 3220 + +/* + * Maximum connection info length as used in walreceiver.h + */ +#define MAXCONNINFO 1024 + + +/* + * pg_stat_replication.sync_state is one if: + * sync, async, quorum, potential + */ +#define PGSR_SYNC_STATE_MAXLENGTH 10 + +/* + * We receive a list of "other nodes" from the monitor, and we store that list + * in local memory. We pre-allocate the memory storage, and limit how many node + * addresses we can handle because of the pre-allocation strategy. + */ +#define NODE_ARRAY_MAX_COUNT 128 + + +/* abstract representation of a Postgres server that we can connect to */ +typedef enum +{ + PGSQL_CONN_LOCAL = 0, + PGSQL_CONN_MONITOR, + PGSQL_CONN_COORDINATOR, + PGSQL_CONN_UPSTREAM, + PGSQL_CONN_APP +} ConnectionType; + + +/* + * Retry policy to follow when we fail to connect to a Postgres URI. + * + * In almost all the code base the retry mechanism is implemented in the main + * loop so we want to fail fast and let the main loop handle the connection + * retry and the different network timeouts that we have, including the network + * partition detection timeout. + * + * In the initialisation code path though, pg_autoctl might be launched from + * provisioning script on a set of nodes in parallel, and in that case we need + * to secure a connection and implement a retry policy at the point in the code + * where we open a connection, so that it's transparent to the caller. + * + * When we do retry connecting, we implement an Exponential Backoff with + * Decorrelated Jitter algorithm as proven useful in the following article: + * + * https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/ + */ +typedef struct ConnectionRetryPolicy +{ + int maxT; /* maximum time spent retrying (seconds) */ + int maxR; /* maximum number of retries, might be zero */ + int maxSleepTime; /* in millisecond, used to cap sleepTime */ + int baseSleepTime; /* in millisecond, base time to sleep for */ + int sleepTime; /* in millisecond, time waited for last round */ + + instr_time startTime; /* time of the first attempt */ + instr_time connectTime; /* time of successful connection */ + int attempts; /* how many attempts have been made so far */ + +#if PG_MAJORVERSION_NUM >= 15 + pg_prng_state prng_state; +#endif +} ConnectionRetryPolicy; + +/* + * Denote if the connetion is going to be used for one, or multiple statements. + * This is used by psql_* functions to know if a connection is to be closed + * after successful completion, or if the the connection is to be maintained + * open for further queries. + * + * A common use case for maintaining a connection open, is while wishing to open + * and maintain a transaction block. Another, is while listening for events. + */ +typedef enum +{ + PGSQL_CONNECTION_SINGLE_STATEMENT = 0, + PGSQL_CONNECTION_MULTI_STATEMENT +} ConnectionStatementType; + +/* + * Allow higher level code to distinguish between failure to connect to the + * target Postgres service and failure to run a query or obtain the expected + * result. To that end we expose PQstatus() of the connection. + * + * We don't use the same enum values as in libpq because we want to have the + * unknown value when we didn't try to connect yet. + */ +typedef enum +{ + PG_CONNECTION_UNKNOWN = 0, + PG_CONNECTION_OK, + PG_CONNECTION_BAD +} PGConnStatus; + +/* notification processing */ +typedef bool (*ProcessNotificationFunction)(int notificationGroupId, + int64_t notificationNodeId, + char *channel, char *payload); + +typedef struct PGSQL +{ + ConnectionType connectionType; + ConnectionStatementType connectionStatementType; + char connectionString[MAXCONNINFO]; + PGconn *connection; + ConnectionRetryPolicy retryPolicy; + PGConnStatus status; + + ProcessNotificationFunction notificationProcessFunction; + int notificationGroupId; + int64_t notificationNodeId; + bool notificationReceived; +} PGSQL; + + +/* PostgreSQL ("Grand Unified Configuration") setting */ +typedef struct GUC +{ + char *name; + char *value; +} GUC; + +/* network address of a node in an HA group */ +typedef struct NodeAddress +{ + int64_t nodeId; + char name[_POSIX_HOST_NAME_MAX]; + char host[_POSIX_HOST_NAME_MAX]; + int port; + int tli; + char lsn[PG_LSN_MAXLENGTH]; + bool isPrimary; +} NodeAddress; + +typedef struct NodeAddressArray +{ + int count; + NodeAddress nodes[NODE_ARRAY_MAX_COUNT]; +} NodeAddressArray; + + +/* + * TimeLineHistoryEntry is taken from Postgres definitions and adapted to + * client-size code where we don't have all the necessary infrastruture. In + * particular we don't define a XLogRecPtr data type nor do we define a + * TimeLineID data type. + * + * Zero is used indicate an invalid pointer. Bootstrap skips the first possible + * WAL segment, initializing the first WAL page at WAL segment size, so no XLOG + * record can begin at zero. + */ +#define InvalidXLogRecPtr 0 +#define XLogRecPtrIsInvalid(r) ((r) == InvalidXLogRecPtr) + +#define PG_AUTOCTL_MAX_TIMELINES 1024 + +typedef struct TimeLineHistoryEntry +{ + uint32_t tli; + uint64_t begin; /* inclusive */ + uint64_t end; /* exclusive, InvalidXLogRecPtr means infinity */ +} TimeLineHistoryEntry; + + +typedef struct TimeLineHistory +{ + int count; + TimeLineHistoryEntry history[PG_AUTOCTL_MAX_TIMELINES]; +} TimeLineHistory; + + +/* + * The IdentifySystem contains information that is parsed from the + * IDENTIFY_SYSTEM replication command, and then the TIMELINE_HISTORY result. + */ +typedef struct IdentifySystem +{ + uint64_t identifier; + uint32_t timeline; + char xlogpos[PG_LSN_MAXLENGTH]; + char dbname[NAMEDATALEN]; + TimeLineHistory timelines; +} IdentifySystem; + + +/* + * The replicationSource structure is used to pass the bits of a connection + * string to the primary node around in several function calls. All the + * information stored in there must fit in a connection string, so MAXCONNINFO + * is a good proxy for their maximum size. + */ +typedef struct ReplicationSource +{ + NodeAddress primaryNode; + char userName[NAMEDATALEN]; + char slotName[MAXCONNINFO]; + char password[MAXCONNINFO]; + char maximumBackupRate[MAXIMUM_BACKUP_RATE_LEN]; + char backupDir[MAXCONNINFO]; + char applicationName[MAXCONNINFO]; + char targetLSN[PG_LSN_MAXLENGTH]; + char targetAction[NAMEDATALEN]; + char targetTimeline[NAMEDATALEN]; + SSLOptions sslOptions; + IdentifySystem system; +} ReplicationSource; + + +/* + * Arrange a generic way to parse PostgreSQL result from a query. Most of the + * queries we need here return a single row of a single column, so that's what + * the default context and parsing allows for. + */ + +/* callback for parsing query results */ +typedef void (ParsePostgresResultCB)(void *context, PGresult *result); + +typedef enum +{ + PGSQL_RESULT_BOOL = 1, + PGSQL_RESULT_INT, + PGSQL_RESULT_BIGINT, + PGSQL_RESULT_STRING +} QueryResultType; + +/* + * As a way to communicate the SQL STATE when an error occurs, every + * pgsql_execute_with_params context structure must have the same first field, + * an array of 5 characters (plus '\0' at the end). + */ +#define SQLSTATE_LENGTH 6 + +#define STR_ERRCODE_CLASS_CONNECTION_EXCEPTION "08" + +typedef struct AbstractResultContext +{ + char sqlstate[SQLSTATE_LENGTH]; +} AbstractResultContext; + +/* data structure for keeping a single-value query result */ +typedef struct SingleValueResultContext +{ + char sqlstate[SQLSTATE_LENGTH]; + QueryResultType resultType; + bool parsedOk; + int ntuples; + bool boolVal; + int intVal; + uint64_t bigint; + char *strVal; +} SingleValueResultContext; + + +#define CHECK__SETTINGS_SQL \ + "select bool_and(ok) " \ + "from (" \ + "select current_setting('max_wal_senders')::int >= 12" \ + " union all " \ + "select current_setting('max_replication_slots')::int >= 12" \ + " union all " \ + "select current_setting('wal_level') in ('replica', 'logical')" \ + " union all " \ + "select current_setting('wal_log_hints') = 'on'" + +#define CHECK_POSTGRESQL_NODE_SETTINGS_SQL \ + CHECK__SETTINGS_SQL \ + ") as t(ok) " + +#define CHECK_CITUS_NODE_SETTINGS_SQL \ + CHECK__SETTINGS_SQL \ + " union all " \ + "select lib = 'citus' " \ + "from unnest(string_to_array(" \ + "current_setting('shared_preload_libraries'), ',') " \ + " || array['not citus']) " \ + "with ordinality ast(lib, n) where n = 1" \ + ") as t(ok) " + +bool pgsql_init(PGSQL *pgsql, char *url, ConnectionType connectionType); + +void pgsql_set_retry_policy(ConnectionRetryPolicy *retryPolicy, + int maxT, + int maxR, + int maxSleepTime, + int baseSleepTime); +void pgsql_set_main_loop_retry_policy(ConnectionRetryPolicy *retryPolicy); +void pgsql_set_init_retry_policy(ConnectionRetryPolicy *retryPolicy); +void pgsql_set_interactive_retry_policy(ConnectionRetryPolicy *retryPolicy); +void pgsql_set_monitor_interactive_retry_policy(ConnectionRetryPolicy *retryPolicy); +int pgsql_compute_connection_retry_sleep_time(ConnectionRetryPolicy *retryPolicy); +bool pgsql_retry_policy_expired(ConnectionRetryPolicy *retryPolicy); + +void pgsql_finish(PGSQL *pgsql); +void parseSingleValueResult(void *ctx, PGresult *result); +void fetchedRows(void *ctx, PGresult *result); +bool pgsql_begin(PGSQL *pgsql); +bool pgsql_commit(PGSQL *pgsql); +bool pgsql_rollback(PGSQL *pgsql); +bool pgsql_execute(PGSQL *pgsql, const char *sql); +bool pgsql_execute_with_params(PGSQL *pgsql, const char *sql, int paramCount, + const Oid *paramTypes, const char **paramValues, + void *parseContext, ParsePostgresResultCB *parseFun); +bool pgsql_check_postgresql_settings(PGSQL *pgsql, bool isCitusInstanceKind, + bool *settings_are_ok); +bool pgsql_check_monitor_settings(PGSQL *pgsql, bool *settings_are_ok); +bool pgsql_is_in_recovery(PGSQL *pgsql, bool *is_in_recovery); +bool pgsql_reload_conf(PGSQL *pgsql); +bool pgsql_replication_slot_exists(PGSQL *pgsql, const char *slotName, + bool *slotExists); +bool pgsql_create_replication_slot(PGSQL *pgsql, const char *slotName); +bool pgsql_drop_replication_slot(PGSQL *pgsql, const char *slotName); +bool postgres_sprintf_replicationSlotName(int64_t nodeId, char *slotName, int size); +bool pgsql_set_synchronous_standby_names(PGSQL *pgsql, + char *synchronous_standby_names); +bool pgsql_replication_slot_create_and_drop(PGSQL *pgsql, + NodeAddressArray *nodeArray); +bool pgsql_replication_slot_maintain(PGSQL *pgsql, NodeAddressArray *nodeArray); +bool pgsql_disable_synchronous_replication(PGSQL *pgsql); +bool pgsql_set_default_transaction_mode_read_only(PGSQL *pgsql); +bool pgsql_set_default_transaction_mode_read_write(PGSQL *pgsql); +bool pgsql_checkpoint(PGSQL *pgsql); +bool pgsql_get_hba_file_path(PGSQL *pgsql, char *hbaFilePath, int maxPathLength); +bool pgsql_create_database(PGSQL *pgsql, const char *dbname, const char *owner); +bool pgsql_create_extension(PGSQL *pgsql, const char *name); +bool pgsql_create_user(PGSQL *pgsql, const char *userName, const char *password, + bool login, bool superuser, bool replication, + int connlimit); +bool pgsql_alter_role_password(PGSQL *pgsql, const char *roleName, + const char *password); +bool pgsql_has_replica(PGSQL *pgsql, char *userName, bool *hasReplica); +bool hostname_from_uri(const char *pguri, + char *hostname, int maxHostLength, int *port); +bool validate_connection_string(const char *connectionString); +bool pgsql_reset_primary_conninfo(PGSQL *pgsql); + +bool pgsql_get_postgres_metadata(PGSQL *pgsql, + bool *pg_is_in_recovery, + char *pgsrSyncState, char *currentLSN, + PostgresControlData *control); + +bool pgsql_one_slot_has_reached_target_lsn(PGSQL *pgsql, + char *targetLSN, + char *currentLSN, + bool *hasReachedLSN); +bool pgsql_has_reached_target_lsn(PGSQL *pgsql, char *targetLSN, + char *currentLSN, bool *hasReachedLSN); +bool pgsql_identify_system(PGSQL *pgsql, IdentifySystem *system); +bool pgsql_listen(PGSQL *pgsql, char *channels[]); +bool pgsql_prepare_to_wait(PGSQL *pgsql); + +bool pgsql_alter_extension_update_to(PGSQL *pgsql, + const char *extname, const char *version); + +bool parseTimeLineHistory(const char *filename, const char *content, + IdentifySystem *system); + + +#endif /* PGSQL_H */ diff --git a/src/bin/common/pgtuning.c b/src/bin/common/pgtuning.c new file mode 100644 index 000000000..50b88eecb --- /dev/null +++ b/src/bin/common/pgtuning.c @@ -0,0 +1,390 @@ +/* + * src/bin/pg_autoctl/pgtuning.c + * Adjust some very basic Postgres tuning to the system properties. + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the PostgreSQL License. + * + */ + +#include "postgres_fe.h" +#include "pqexpbuffer.h" + +#include "config.h" +#include "env_utils.h" +#include "file_utils.h" +#include "log.h" +#include "pgctl.h" +#include "pgtuning.h" +#include "system_utils.h" + +/* + * In most cases we are going to initdb a Postgres instance for our users, we + * might as well introduce some naive Postgres tuning. In the static array are + * selected Postgres default values and static values we always set. + * + * Dynamic code is then used on the target systems to compute better values + * dynamically for some parameters: work_mem, maintenance_work_mem, + * effective_cache_size, autovacuum_max_workers. + */ +GUC postgres_tuning[] = { + { "track_functions", "pl" }, + { "shared_buffers", "'128 MB'" }, + { "work_mem", "'4 MB'" }, + { "maintenance_work_mem", "'64MB'" }, + { "effective_cache_size", "'4 GB'" }, + { "autovacuum_max_workers", "3" }, + { "autovacuum_vacuum_scale_factor", "0.08" }, + { "autovacuum_analyze_scale_factor", "0.02" }, + { NULL, NULL } +}; + + +typedef struct DynamicTuning +{ + int autovacuum_max_workers; + uint64_t shared_buffers; + uint64_t work_mem; + uint64_t maintenance_work_mem; + uint64_t effective_cache_size; +} DynamicTuning; + + +static bool pgtuning_compute_mem_settings(SystemInfo *sysInfo, + DynamicTuning *tuning); + +void pgtuning_log_settings(DynamicTuning *tuning, int logLevel); + +static int pgtuning_compute_max_workers(SystemInfo *sysInfo); + +static bool pgtuning_edit_guc_settings(GUC *settings, DynamicTuning *tuning, + char *config, size_t size); + + +/* + * pgtuning_prepare_guc_settings probes the system information (nCPU and total + * RAM) and computes some better defaults for Postgres. + */ +bool +pgtuning_prepare_guc_settings(GUC *settings, char *config, size_t size) +{ + SystemInfo sysInfo = { 0 }; + DynamicTuning tuning = { 0 }; + char totalram[BUFSIZE] = { 0 }; + + if (!get_system_info(&sysInfo)) + { + /* errors have already been logged */ + return false; + } + + (void) pretty_print_bytes(totalram, sizeof(totalram), sysInfo.totalram); + + log_debug("Detected %d CPUs and %s total RAM on this server", + sysInfo.ncpu, + totalram); + + /* + * Disable Postgres tuning when running the unit test suite: we install our + * default set of values rather than computing better values for the + * current environment. + */ + if (!(env_exists(PG_AUTOCTL_DEBUG) && env_exists("PG_REGRESS_SOCK_DIR"))) + { + tuning.autovacuum_max_workers = pgtuning_compute_max_workers(&sysInfo); + + if (!pgtuning_compute_mem_settings(&sysInfo, &tuning)) + { + log_error("Failed to compute memory settings, using defaults"); + return false; + } + + (void) pgtuning_log_settings(&tuning, LOG_DEBUG); + } + + return pgtuning_edit_guc_settings(settings, &tuning, config, size); +} + + +/* + * pgtuning_compute_max_workers returns how many autovacuum max workers we can + * setup on the local system, depending on its number of CPUs. + * + * We could certainly cook a simple enough maths expression to compute the + * numbers assigned in this range based "grid" here, but that would be much + * harder to maintain and change our mind about, and not as easy to grasp on a + * quick reading. + */ +static int +pgtuning_compute_max_workers(SystemInfo *sysInfo) +{ + /* use the default up to 16 cores (HT included) */ + if (sysInfo->ncpu < 16) + { + return 3; + } + else if (sysInfo->ncpu < 24) + { + return 4; + } + else if (sysInfo->ncpu < 32) + { + return 6; + } + else if (sysInfo->ncpu < 48) + { + return 8; + } + else if (sysInfo->ncpu < 64) + { + return 12; + } + else + { + return 16; + } +} + + +/* + * pgtuning_compute_work_mem computes how much work mem to use on this system. + * + * Inspiration has been taken from http://pgconfigurator.cybertec.at + * + * Rather than trying to devise a good maths expression to compute values, we + * implement our decision making with a range based approach. Some values are + * still computed with an expression (shared_buffers is set to 25% of the total + * RAM up to 256 GB of RAM, for instance). + */ +static bool +pgtuning_compute_mem_settings(SystemInfo *sysInfo, DynamicTuning *tuning) +{ + uint64_t oneGB = ((uint64_t) 1) << 30; + + /* + * <= 8 GB of RAM + */ + if (sysInfo->totalram <= (8 * oneGB)) + { + tuning->shared_buffers = sysInfo->totalram / 4; + tuning->work_mem = 16 * 1 << 20; /* 16 MB */ + tuning->maintenance_work_mem = 256 * 1 << 20; /* 256 MB */ + } + + /* + * > 8 GB up to 64 GB of RAM + */ + else if (sysInfo->totalram <= (64 * oneGB)) + { + tuning->shared_buffers = sysInfo->totalram / 4; + tuning->work_mem = 24 * 1 << 20; /* 24 MB */ + tuning->maintenance_work_mem = 512 * 1 << 20; /* 512 MB */ + } + + /* + * > 64 GB up to 256 GB of RAM + */ + else if (sysInfo->totalram <= (256 * oneGB)) + { + tuning->shared_buffers = 16 * oneGB; /* 16 GB */ + tuning->work_mem = 32 * 1 << 20; /* 32 MB */ + tuning->maintenance_work_mem = oneGB; /* 1 GB */ + } + + /* + * > 256 GB of RAM + */ + else + { + tuning->shared_buffers = 32 * oneGB; /* 32 GB */ + tuning->work_mem = 64 * 1 << 20; /* 64 MB */ + tuning->maintenance_work_mem = 2 * oneGB; /* 2 GB */ + } + + /* + * What's not in shared buffers is expected to be mostly file system cache, + * and then again effective_cache_size is a hint and does not need to be + * the exact value as shown by the free(1) command. + */ + tuning->effective_cache_size = sysInfo->totalram - tuning->shared_buffers; + + return true; +} + + +/* + * pgtuning_log_mem_settings logs the memory settings we computed. + */ +void +pgtuning_log_settings(DynamicTuning *tuning, int logLevel) +{ + char buf[BUFSIZE] = { 0 }; + + log_level(logLevel, + "Setting autovacuum_max_workers to %d", + tuning->autovacuum_max_workers); + + (void) pretty_print_bytes(buf, sizeof(buf), tuning->shared_buffers); + log_level(logLevel, "Setting shared_buffers to %s", buf); + + (void) pretty_print_bytes(buf, sizeof(buf), tuning->work_mem); + log_level(logLevel, "Setting work_mem to %s", buf); + + (void) pretty_print_bytes(buf, sizeof(buf), + tuning->maintenance_work_mem); + log_level(logLevel, "Setting maintenance_work_mem to %s", buf); + + (void) pretty_print_bytes(buf, sizeof(buf), + tuning->effective_cache_size); + log_level(logLevel, "Setting effective_cache_size to %s", buf); +} + + +/* + * pgtuning_edit_guc_settings prepares a Postgres configuration file snippet + * from the given GUC settings and the dynamic tuning adjusted to the system + * and place the resulting snippet in the pre-allocated string buffer config of + * given size. + */ +#define streq(x, y) ((x != NULL) && (y != NULL) && (strcmp(x, y) == 0)) + +static bool +pgtuning_edit_guc_settings(GUC *settings, DynamicTuning *tuning, + char *config, size_t size) +{ + PQExpBuffer contents = createPQExpBuffer(); + int settingIndex = 0; + + if (contents == NULL) + { + log_error("Failed to allocate memory"); + return false; + } + + appendPQExpBuffer(contents, + "# basic tuning computed by pg_auto_failover\n"); + + /* replace placeholder values with dynamic tuned values */ + for (settingIndex = 0; settings[settingIndex].name != NULL; settingIndex++) + { + GUC *setting = &settings[settingIndex]; + + if (streq(setting->name, "autovacuum_max_workers")) + { + if (tuning->autovacuum_max_workers > 0) + { + appendPQExpBuffer(contents, "%s = %d\n", + setting->name, + tuning->autovacuum_max_workers); + } + else + { + appendPQExpBuffer(contents, "%s = %s\n", + setting->name, + setting->value); + } + } + else if (streq(setting->name, "shared_buffers")) + { + if (tuning->shared_buffers > 0) + { + char pretty[BUFSIZE] = { 0 }; + + (void) pretty_print_bytes(pretty, sizeof(pretty), + tuning->shared_buffers); + + appendPQExpBuffer(contents, "%s = '%s'\n", + setting->name, pretty); + } + else + { + appendPQExpBuffer(contents, "%s = %s\n", + setting->name, setting->value); + } + } + else if (streq(setting->name, "work_mem")) + { + if (tuning->work_mem > 0) + { + char pretty[BUFSIZE] = { 0 }; + + (void) pretty_print_bytes(pretty, sizeof(pretty), + tuning->work_mem); + + appendPQExpBuffer(contents, "%s = '%s'\n", + setting->name, pretty); + } + else + { + appendPQExpBuffer(contents, "%s = %s\n", + setting->name, setting->value); + } + } + else if (streq(setting->name, "maintenance_work_mem")) + { + if (tuning->maintenance_work_mem > 0) + { + char pretty[BUFSIZE] = { 0 }; + + (void) pretty_print_bytes(pretty, sizeof(pretty), + tuning->maintenance_work_mem); + + appendPQExpBuffer(contents, "%s = '%s'\n", + setting->name, pretty); + } + else + { + appendPQExpBuffer(contents, "%s = %s\n", + setting->name, setting->value); + } + } + else if (streq(setting->name, "effective_cache_size")) + { + if (tuning->effective_cache_size > 0) + { + char pretty[BUFSIZE] = { 0 }; + + (void) pretty_print_bytes(pretty, sizeof(pretty), + tuning->effective_cache_size); + + appendPQExpBuffer(contents, "%s = '%s'\n", + setting->name, pretty); + } + else + { + appendPQExpBuffer(contents, "%s = %s\n", + setting->name, setting->value); + } + } + else + { + appendPQExpBuffer(contents, "%s = %s\n", + setting->name, setting->value); + } + } + + /* memory allocation could have failed while building string */ + if (PQExpBufferBroken(contents)) + { + log_error("Failed to allocate memory"); + destroyPQExpBuffer(contents); + return false; + } + + if (size < contents->len) + { + log_error("Failed to prepare Postgres tuning for the local system, " + "the setup needs %lu bytes and pg_autoctl only support " + "up to %lu bytes", + (unsigned long) contents->len, + (unsigned long) size); + destroyPQExpBuffer(contents); + return false; + } + + strlcpy(config, contents->data, size); + + destroyPQExpBuffer(contents); + + return true; +} diff --git a/src/bin/common/pgtuning.h b/src/bin/common/pgtuning.h new file mode 100644 index 000000000..de4a98044 --- /dev/null +++ b/src/bin/common/pgtuning.h @@ -0,0 +1,19 @@ +/* + * src/bin/pg_autoctl/pgtuning.h + * Adjust some very basic Postgres tuning to the system properties. + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the PostgreSQL License. + * + */ + +#ifndef PGTUNING_H +#define PGTUNING_H + +#include + +extern GUC postgres_tuning[]; + +bool pgtuning_prepare_guc_settings(GUC *settings, char *config, size_t size); + +#endif /* PGTUNING_H */ diff --git a/src/bin/common/pidfile.c b/src/bin/common/pidfile.c new file mode 100644 index 000000000..931b67ed9 --- /dev/null +++ b/src/bin/common/pidfile.c @@ -0,0 +1,568 @@ +/* + * src/bin/pg_autoctl/pidfile.c + * Utilities to manage the pg_autoctl pidfile. + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the PostgreSQL License. + * + */ + +#include +#include +#include +#include +#include +#include + +#include "postgres_fe.h" +#include "pqexpbuffer.h" + +#include "cli_common.h" +#include "cli_root.h" +#include "defaults.h" +#include "env_utils.h" +#include "fsm.h" +#include "keeper.h" +#include "keeper_config.h" +#include "keeper_pg_init.h" +#include "lock_utils.h" +#include "log.h" +#include "monitor.h" +#include "pgctl.h" +#include "pidfile.h" +#include "state.h" +#include "signals.h" +#include "string_utils.h" + +/* pidfile for this process */ +char service_pidfile[MAXPGPATH] = { 0 }; + +static void remove_service_pidfile_atexit(void); + +/* + * create_pidfile writes our pid in a file. + * + * When running in a background loop, we need a pidFile to add a command line + * tool that send signals to the process. The pidfile has a single line + * containing our PID. + */ +bool +create_pidfile(const char *pidfile, pid_t pid) +{ + PQExpBuffer content = createPQExpBuffer(); + + + log_trace("create_pidfile(%d): \"%s\"", pid, pidfile); + + if (content == NULL) + { + log_fatal("Failed to allocate memory to update our PID file"); + return false; + } + + if (!prepare_pidfile_buffer(content, pid)) + { + /* errors have already been logged */ + destroyPQExpBuffer(content); + return false; + } + + + /* memory allocation could have failed while building string */ + if (PQExpBufferBroken(content)) + { + log_error("Failed to create pidfile \"%s\": out of memory", pidfile); + destroyPQExpBuffer(content); + return false; + } + + bool success = write_file(content->data, content->len, pidfile); + destroyPQExpBuffer(content); + + return success; +} + + +/* + * prepare_pidfile_buffer prepares a PQExpBuffer content with the information + * expected to be found in a pidfile. + */ +bool +prepare_pidfile_buffer(PQExpBuffer content, pid_t pid) +{ + char pgdata[MAXPGPATH] = { 0 }; + + /* we get PGDATA from the environment */ + if (!get_env_pgdata(pgdata)) + { + log_fatal("Failed to get PGDATA to create the PID file"); + return false; + } + + /* + * line # + * 1 supervisor PID + * 2 data directory path + * 3 version number (PG_AUTOCTL_VERSION) + * 4 extension version number (PG_AUTOCTL_EXTENSION_VERSION) + * 5 shared semaphore id (used to serialize log writes) + */ + appendPQExpBuffer(content, "%d\n", pid); + appendPQExpBuffer(content, "%s\n", pgdata); + appendPQExpBuffer(content, "%s\n", PG_AUTOCTL_VERSION); + appendPQExpBuffer(content, "%s\n", PG_AUTOCTL_EXTENSION_VERSION); + appendPQExpBuffer(content, "%d\n", log_semaphore.semId); + + return true; +} + + +/* + * create_pidfile writes the given serviceName pidfile, using getpid(). + */ +bool +create_service_pidfile(const char *pidfile, const char *serviceName) +{ + pid_t pid = getpid(); + + /* compute the service pidfile and store it in our global variable */ + (void) get_service_pidfile(pidfile, serviceName, service_pidfile); + + /* register our service pidfile clean-up atexit */ + atexit(remove_service_pidfile_atexit); + + return create_pidfile(service_pidfile, pid); +} + + +/* + * get_service_pidfile computes the pidfile names for the given service. + */ +void +get_service_pidfile(const char *pidfile, + const char *serviceName, + char *servicePidFilename) +{ + char filename[MAXPGPATH] = { 0 }; + + sformat(filename, sizeof(filename), "pg_autoctl_%s.pid", serviceName); + path_in_same_directory(pidfile, filename, servicePidFilename); +} + + +/* + * remove_service_pidfile_atexit is called atexit() to remove the service + * pidfile. + */ +static void +remove_service_pidfile_atexit() +{ + (void) remove_pidfile(service_pidfile); +} + + +/* + * read_pidfile read pg_autoctl pid from a file, and returns true when we could + * read a PID that belongs to a currently running process. + */ +bool +read_pidfile(const char *pidfile, pid_t *pid) +{ + long fileSize = 0L; + char *fileContents = NULL; + char *fileLines[1]; + int pidnum = 0; + + if (!file_exists(pidfile)) + { + return false; + } + + if (!read_file(pidfile, &fileContents, &fileSize)) + { + log_debug("Failed to read the PID file \"%s\", removing it", pidfile); + (void) remove_pidfile(pidfile); + + return false; + } + + splitLines(fileContents, fileLines, 1); + stringToInt(fileLines[0], &pidnum); + + *pid = pidnum; + + free(fileContents); + + if (*pid <= 0) + { + log_debug("Read negative pid %d in file \"%s\", removing it", + *pid, pidfile); + (void) remove_pidfile(pidfile); + + return false; + } + + /* is it a stale file? */ + if (kill(*pid, 0) == 0) + { + return true; + } + else + { + log_debug("Failed to signal pid %d: %m", *pid); + *pid = 0; + + log_info("Found a stale pidfile at \"%s\"", pidfile); + log_warn("Removing the stale pid file \"%s\"", pidfile); + + /* + * We must return false here, after having determined that the + * pidfile belongs to a process that doesn't exist anymore. So we + * remove the pidfile and don't take the return value into account + * at this point. + */ + (void) remove_pidfile(pidfile); + + /* we might have to cleanup a stale SysV semaphore, too */ + (void) semaphore_cleanup(pidfile); + + return false; + } +} + + +/* + * remove_pidfile removes pg_autoctl pidfile. + */ +bool +remove_pidfile(const char *pidfile) +{ + if (remove(pidfile) != 0) + { + log_error("Failed to remove pid file \"%s\": %m", pidfile); + return false; + } + return true; +} + + +/* + * check_pidfile checks that the given PID file still contains the known pid of + * the service. If the file is owned by another process, just quit immediately. + */ +void +check_pidfile(const char *pidfile, pid_t start_pid) +{ + pid_t checkpid = 0; + + /* + * It might happen that the PID file got removed from disk, then + * allowing another process to run. + * + * We should then quit in an emergency if our PID file either doesn't + * exist anymore, or has been overwritten with another PID. + * + */ + if (read_pidfile(pidfile, &checkpid)) + { + if (checkpid != start_pid) + { + log_fatal("Our PID file \"%s\" now contains PID %d, " + "instead of expected pid %d. Quitting.", + pidfile, checkpid, start_pid); + + exit(EXIT_CODE_QUIT); + } + } + else + { + /* + * Surrendering seems the less risky option for us now. + * + * Any other strategy would need to be careful about race conditions + * happening when several processes (keeper or others) are trying to + * create or remove the pidfile at the same time, possibly in different + * orders. Yeah, let's quit. + */ + log_fatal("PID file not found at \"%s\", quitting.", pidfile); + exit(EXIT_CODE_QUIT); + } +} + + +/* + * read_service_pidfile_version_string reads a service pidfile and copies the + * version string found on line PIDFILE_LINE_VERSION_STRING into the + * pre-allocated buffer versionString. + */ +bool +read_service_pidfile_version_strings(const char *pidfile, + char *versionString, + char *extensionVersionString) +{ + long fileSize = 0L; + char *fileContents = NULL; + char *fileLines[BUFSIZE] = { 0 }; + int lineNumber; + + if (!read_file_if_exists(pidfile, &fileContents, &fileSize)) + { + return false; + } + + int lineCount = splitLines(fileContents, fileLines, BUFSIZE); + + for (lineNumber = 0; lineNumber < lineCount; lineNumber++) + { + int pidLine = lineNumber + 1; /* zero-based, one-based */ + + /* version string */ + if (pidLine == PIDFILE_LINE_VERSION_STRING) + { + strlcpy(versionString, fileLines[lineNumber], BUFSIZE); + } + + /* extension version string, comes later in the file */ + if (pidLine == PIDFILE_LINE_EXTENSION_VERSION) + { + strlcpy(extensionVersionString, fileLines[lineNumber], BUFSIZE); + free(fileContents); + return true; + } + } + + free(fileContents); + + return false; +} + + +/* + * fprint_pidfile_as_json prints the content of the pidfile as JSON. + * + * When includeStatus is true, add a "status" entry for each PID (main service + * and sub-processes) with either "running" or "stale" as a value, depending on + * what a kill -0 reports. + */ +void +pidfile_as_json(JSON_Value *js, const char *pidfile, bool includeStatus) +{ + JSON_Value *jsServices = json_value_init_array(); + JSON_Array *jsServicesArray = json_value_get_array(jsServices); + + JSON_Object *jsobj = json_value_get_object(js); + + long fileSize = 0L; + char *fileContents = NULL; + char *fileLines[BUFSIZE] = { 0 }; + int lineNumber; + + if (!read_file_if_exists(pidfile, &fileContents, &fileSize)) + { + exit(EXIT_CODE_INTERNAL_ERROR); + } + + int lineCount = splitLines(fileContents, fileLines, BUFSIZE); + + for (lineNumber = 0; lineNumber < lineCount; lineNumber++) + { + int pidLine = lineNumber + 1; /* zero-based, one-based */ + char *separator = NULL; + + /* main pid */ + if (pidLine == PIDFILE_LINE_PID) + { + int pidnum = 0; + stringToInt(fileLines[lineNumber], &pidnum); + json_object_set_number(jsobj, "pid", (double) pidnum); + + if (includeStatus) + { + if (kill(pidnum, 0) == 0) + { + json_object_set_string(jsobj, "status", "running"); + } + else + { + json_object_set_string(jsobj, "status", "stale"); + } + } + continue; + } + + /* data directory */ + if (pidLine == PIDFILE_LINE_DATA_DIR) + { + json_object_set_string(jsobj, "pgdata", fileLines[lineNumber]); + } + + /* version string */ + if (pidLine == PIDFILE_LINE_VERSION_STRING) + { + json_object_set_string(jsobj, "version", fileLines[lineNumber]); + } + + /* extension version string */ + if (pidLine == PIDFILE_LINE_EXTENSION_VERSION) + { + /* skip it, the supervisor does not connect to the monitor */ + (void) 0; + } + + /* semId */ + if (pidLine == PIDFILE_LINE_SEM_ID) + { + int semId = 0; + + if (stringToInt(fileLines[lineNumber], &semId)) + { + json_object_set_number(jsobj, "semId", (double) semId); + } + else + { + log_error("Failed to parse semId \"%s\"", fileLines[lineNumber]); + } + + continue; + } + + if (pidLine >= PIDFILE_LINE_FIRST_SERVICE) + { + JSON_Value *jsService = json_value_init_object(); + JSON_Object *jsServiceObj = json_value_get_object(jsService); + + if ((separator = strchr(fileLines[lineNumber], ' ')) == NULL) + { + log_debug("Failed to find a space separator in line: \"%s\"", + fileLines[lineNumber]); + continue; + } + else + { + int pidnum = 0; + char *serviceName = separator + 1; + + char servicePidFile[BUFSIZE] = { 0 }; + + char versionString[BUFSIZE] = { 0 }; + char extensionVersionString[BUFSIZE] = { 0 }; + + *separator = '\0'; + stringToInt(fileLines[lineNumber], &pidnum); + + json_object_set_string(jsServiceObj, "name", serviceName); + json_object_set_number(jsServiceObj, "pid", pidnum); + + if (includeStatus) + { + if (kill(pidnum, 0) == 0) + { + json_object_set_string(jsServiceObj, "status", "running"); + } + else + { + json_object_set_string(jsServiceObj, "status", "stale"); + } + } + + /* grab version number of the service by parsing its pidfile */ + get_service_pidfile(pidfile, serviceName, servicePidFile); + + if (!read_service_pidfile_version_strings( + servicePidFile, + versionString, + extensionVersionString)) + { + /* warn about it and continue */ + log_warn("Failed to read version string for " + "service \"%s\" in pidfile \"%s\"", + serviceName, + servicePidFile); + } + else + { + json_object_set_string(jsServiceObj, + "version", versionString); + json_object_set_string(jsServiceObj, + "pgautofailover", + extensionVersionString); + } + } + + json_array_append_value(jsServicesArray, jsService); + } + } + + json_object_set_value(jsobj, "services", jsServices); + + free(fileContents); +} + + +/* + * is_process_stopped reads given pidfile and checks if the included PID + * belongs to a process that's still running, and if not, sets the *stopped + * boolean to true. + */ +bool +is_process_stopped(const char *pidfile, bool *stopped, pid_t *pid) +{ + if (!file_exists(pidfile)) + { + *stopped = true; + return true; + } + + if (!read_pidfile(pidfile, pid)) + { + log_error("Failed to read PID file \"%s\"", pidfile); + return false; + } + + *stopped = false; + return true; +} + + +/* + * wait_for_process_to_stop waits until the PID found in the pidfile is not + * running anymore. + */ +bool +wait_for_process_to_stop(const char *pidfile, int timeout, bool *stopped, pid_t *pid) +{ + if (!is_process_stopped(pidfile, stopped, pid)) + { + /* errors have already been logged */ + return false; + } + + /* if the process has stopped already, we're done here */ + if (*stopped) + { + return true; + } + + log_info("An instance of pg_autoctl is running with PID %d, " + "waiting for it to stop.", *pid); + + int timeout_counter = timeout; + + while (timeout_counter > 0) + { + if (kill(*pid, 0) == -1 && errno == ESRCH) + { + log_info("The pg_autoctl instance with pid %d " + "has now terminated.", + *pid); + *stopped = true; + return true; + } + + sleep(1); + --timeout_counter; + } + + *stopped = false; + return true; +} diff --git a/src/bin/common/pidfile.h b/src/bin/common/pidfile.h new file mode 100644 index 000000000..749bfd083 --- /dev/null +++ b/src/bin/common/pidfile.h @@ -0,0 +1,74 @@ +/* + * src/bin/pg_autoctl/pidfile.h + * Utilities to manage the pg_autoctl pidfile. + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the PostgreSQL License. + * + */ +#ifndef PIDFILE_H +#define PIDFILE_H + +#include +#include + +#include "postgres_fe.h" +#include "pqexpbuffer.h" + +#include "keeper.h" +#include "keeper_config.h" +#include "monitor.h" +#include "monitor_config.h" + +/* + * As of pg_autoctl 1.4, the contents of the pidfile is: + * + * line # + * 1 supervisor PID + * 2 data directory path + * 3 version number (PG_AUTOCTL_VERSION) + * 4 extension version number (PG_AUTOCTL_EXTENSION_VERSION) + * 5 shared semaphore id (used to serialize log writes) + * 6 first supervised service pid line + * 7 second supervised service pid line + * ... + * + * The supervised service lines are added later, not the first time we create + * the pidfile. Each service line contains 2 bits of information, separated + * with spaces: + * + * pid service-name + * + * Each service creates its own pidfile with its own version number. At + * pg_autoctl upgrade time, we might have a supervisor process that's running + * with a different version than one of the restarted pg_autoctl services. + */ +#define PIDFILE_LINE_PID 1 +#define PIDFILE_LINE_DATA_DIR 2 +#define PIDFILE_LINE_VERSION_STRING 3 +#define PIDFILE_LINE_EXTENSION_VERSION 4 +#define PIDFILE_LINE_SEM_ID 5 +#define PIDFILE_LINE_FIRST_SERVICE 6 + +bool create_pidfile(const char *pidfile, pid_t pid); + +bool prepare_pidfile_buffer(PQExpBuffer content, pid_t pid); +bool create_service_pidfile(const char *pidfile, const char *serviceName); +void get_service_pidfile(const char *pidfile, + const char *serviceName, + char *filename); +bool read_service_pidfile_version_strings(const char *pidfile, + char *versionString, + char *extensionVersionString); + +bool read_pidfile(const char *pidfile, pid_t *pid); +bool remove_pidfile(const char *pidfile); +void check_pidfile(const char *pidfile, pid_t start_pid); + +void pidfile_as_json(JSON_Value *js, const char *pidfile, bool includeStatus); + +bool is_process_stopped(const char *pidfile, bool *stopped, pid_t *pid); +bool wait_for_process_to_stop(const char *pidfile, int timeout, bool *stopped, + pid_t *pid); + +#endif /* PIDFILE_H */ diff --git a/src/bin/common/signals.c b/src/bin/common/signals.c new file mode 100644 index 000000000..a870d66f5 --- /dev/null +++ b/src/bin/common/signals.c @@ -0,0 +1,260 @@ +/* + * src/bin/pg_autoctl/signals.c + * Signal handlers for pg_autoctl, used in loop.c and pgsetup.c + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the PostgreSQL License. + * + */ + +#include +#include +#include +#include + +#include "postgres_fe.h" /* pqsignal, portable sigaction wrapper */ + +#include "cli_root.h" +#include "defaults.h" +#include "lock_utils.h" +#include "log.h" +#include "signals.h" + +/* This flag controls termination of the main loop. */ +volatile sig_atomic_t asked_to_stop = 0; /* SIGTERM */ +volatile sig_atomic_t asked_to_stop_fast = 0; /* SIGINT */ +volatile sig_atomic_t asked_to_reload = 0; /* SIGHUP */ +volatile sig_atomic_t asked_to_quit = 0; /* SIGQUIT */ + +/* + * set_signal_handlers sets our signal handlers for the 4 signals that we + * specifically handle in pg_autoctl. + */ +void +set_signal_handlers(bool exitOnQuit) +{ + /* Establish a handler for signals. */ + log_trace("set_signal_handlers%s", exitOnQuit ? " (exit on quit)" : ""); + + pqsignal(SIGHUP, catch_reload); + pqsignal(SIGINT, catch_int); + pqsignal(SIGTERM, catch_term); + + if (exitOnQuit) + { + pqsignal(SIGQUIT, catch_quit_and_exit); + } + else + { + pqsignal(SIGQUIT, catch_quit); + } +} + + +/* + * mask_signals prepares a pselect() call by masking all the signals we handle + * in this part of the code, to avoid race conditions with setting our atomic + * variables at signal handling. + */ +bool +block_signals(sigset_t *mask, sigset_t *orig_mask) +{ + int signals[] = { SIGHUP, SIGINT, SIGTERM, SIGQUIT, -1 }; + + if (sigemptyset(mask) == -1) + { + /* man sigemptyset sayth: No errors are defined. */ + log_error("sigemptyset: %m"); + return false; + } + + for (int i = 0; signals[i] != -1; i++) + { + /* + * The sigaddset() function may fail if: + * + * EINVAL The value of the signo argument is an invalid or unsupported + * signal number + * + * This should never happen given the manual set of signals we are + * processing here in this loop. + */ + if (sigaddset(mask, signals[i]) == -1) + { + log_error("sigaddset: %m"); + return false; + } + } + + if (sigprocmask(SIG_BLOCK, mask, orig_mask) == -1) + { + log_error("Failed to block signals: sigprocmask: %m"); + return false; + } + + return true; +} + + +/* + * unblock_signals calls sigprocmask to re-establish the normal signal mask, in + * order to allow our code to handle signals again. + * + * If we fail to unblock signals, then we won't be able to react to any + * interruption, reload, or shutdown sequence, and we'd rather exit now. + */ +void +unblock_signals(sigset_t *orig_mask) +{ + /* restore signal masks (un block them) now */ + if (sigprocmask(SIG_SETMASK, orig_mask, NULL) == -1) + { + log_fatal("Failed to restore signals: sigprocmask: %m"); + exit(EXIT_CODE_INTERNAL_ERROR); + } +} + + +/* + * catch_reload receives the SIGHUP signal. + */ +void +catch_reload(int sig) +{ + asked_to_reload = 1; + pqsignal(sig, catch_reload); +} + + +/* + * catch_int receives the SIGINT signal. + */ +void +catch_int(int sig) +{ + asked_to_stop_fast = 1; + pqsignal(sig, catch_int); +} + + +/* + * catch_stop receives SIGTERM signal. + */ +void +catch_term(int sig) +{ + asked_to_stop = 1; + pqsignal(sig, catch_term); +} + + +/* + * catch_quit receives the SIGQUIT signal. + */ +void +catch_quit(int sig) +{ + /* default signal handler disposition is to core dump, we don't */ + asked_to_quit = 1; + pqsignal(sig, catch_quit); +} + + +/* + * quit_and_exit exit(EXIT_CODE_QUIT) upon receiving the SIGQUIT signal. + */ +void +catch_quit_and_exit(int sig) +{ + /* default signal handler disposition is to core dump, we don't */ + exit(EXIT_CODE_QUIT); +} + + +/* + * get_current_signal returns the current signal to process and gives a prioriy + * towards SIGQUIT, then SIGINT, then SIGTERM. + */ +int +get_current_signal(int defaultSignal) +{ + if (asked_to_quit) + { + return SIGQUIT; + } + else if (asked_to_stop_fast) + { + return SIGINT; + } + else if (asked_to_stop) + { + return SIGTERM; + } + + /* no termination signal to process at this time, return the default */ + return defaultSignal; +} + + +/* + * pick_stronger_signal returns the "stronger" signal among the two given + * arguments. + * + * Signal processing have a priority or hierarchy of their own. Once we have + * received and processed SIGQUIT we want to stay at this signal level. Once we + * have received SIGINT we may upgrade to SIGQUIT, but we won't downgrade to + * SIGTERM. + */ +int +pick_stronger_signal(int sig1, int sig2) +{ + if (sig1 == SIGQUIT || sig2 == SIGQUIT) + { + return SIGQUIT; + } + else if (sig1 == SIGINT || sig2 == SIGINT) + { + return SIGINT; + } + else + { + return SIGTERM; + } +} + + +/* + * signal_to_string is our own specialised function to display a signal. The + * strsignal() output does not look like what we need. + */ +char * +signal_to_string(int signal) +{ + switch (signal) + { + case SIGQUIT: + { + return "SIGQUIT"; + } + + case SIGTERM: + { + return "SIGTERM"; + } + + case SIGINT: + { + return "SIGINT"; + } + + case SIGHUP: + { + return "SIGHUP"; + } + + default: + { + return "unknown signal"; + } + } +} diff --git a/src/bin/common/signals.h b/src/bin/common/signals.h new file mode 100644 index 000000000..3c6d27807 --- /dev/null +++ b/src/bin/common/signals.h @@ -0,0 +1,38 @@ +/* + * src/bin/pg_autoctl/signals.h + * Signal handlers for pg_autoctl, used in loop.c and pgsetup.c + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the PostgreSQL License. + * + */ + +#ifndef SIGNALS_H +#define SIGNALS_H + +#include +#include + +/* This flag controls termination of the main loop. */ +extern volatile sig_atomic_t asked_to_stop; /* SIGTERM */ +extern volatile sig_atomic_t asked_to_stop_fast; /* SIGINT */ +extern volatile sig_atomic_t asked_to_reload; /* SIGHUP */ +extern volatile sig_atomic_t asked_to_quit; /* SIGQUIT */ + +#define CHECK_FOR_FAST_SHUTDOWN { if (asked_to_stop_fast) { break; } \ +} + +void set_signal_handlers(bool exitOnQuit); +bool block_signals(sigset_t *mask, sigset_t *orig_mask); +void unblock_signals(sigset_t *orig_mask); +void catch_reload(int sig); +void catch_int(int sig); +void catch_term(int sig); +void catch_quit(int sig); +void catch_quit_and_exit(int sig); + +int get_current_signal(int defaultSignal); +int pick_stronger_signal(int sig1, int sig2); +char * signal_to_string(int signal); + +#endif /* SIGNALS_H */ diff --git a/src/bin/common/string_utils.c b/src/bin/common/string_utils.c new file mode 100644 index 000000000..c9d38f2c2 --- /dev/null +++ b/src/bin/common/string_utils.c @@ -0,0 +1,601 @@ +/* + * src/bin/pg_autoctl/string_utils.c + * Implementations of utility functions for string handling + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the PostgreSQL License. + * + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "postgres_fe.h" +#include "pqexpbuffer.h" + +#include "defaults.h" +#include "file_utils.h" +#include "log.h" +#include "parsing.h" +#include "string_utils.h" + +/* + * intToString converts an int to an IntString, which contains a decimal string + * representation of the integer. + */ +IntString +intToString(int64_t number) +{ + IntString intString; + + intString.intValue = number; + + sformat(intString.strValue, INTSTRING_MAX_DIGITS, "%" PRId64, number); + + return intString; +} + + +/* + * converts given string to 64 bit integer value. + * returns 0 upon failure and sets error flag + */ +bool +stringToInt(const char *str, int *number) +{ + char *endptr; + + if (str == NULL) + { + return false; + } + + if (number == NULL) + { + return false; + } + + errno = 0; + + long long int n = strtoll(str, &endptr, 10); + + if (str == endptr) + { + return false; + } + else if (errno != 0) + { + return false; + } + else if (*endptr != '\0') + { + return false; + } + else if (n < INT_MIN || n > INT_MAX) + { + return false; + } + + *number = n; + + return true; +} + + +/* + * converts given string to 64 bit integer value. + * returns 0 upon failure and sets error flag + */ +bool +stringToInt64(const char *str, int64_t *number) +{ + char *endptr; + + if (str == NULL) + { + return false; + } + + if (number == NULL) + { + return false; + } + + errno = 0; + + long long int n = strtoll(str, &endptr, 10); + + if (str == endptr) + { + return false; + } + else if (errno != 0) + { + return false; + } + else if (*endptr != '\0') + { + return false; + } + else if (n < INT64_MIN || n > INT64_MAX) + { + return false; + } + + *number = n; + + return true; +} + + +/* + * converts given string to 64 bit unsigned integer value. + * returns 0 upon failure and sets error flag + */ +bool +stringToUInt(const char *str, unsigned int *number) +{ + char *endptr; + + if (str == NULL) + { + return false; + } + + if (number == NULL) + { + return false; + } + + errno = 0; + unsigned long long n = strtoull(str, &endptr, 10); + + if (str == endptr) + { + return false; + } + else if (errno != 0) + { + return false; + } + else if (*endptr != '\0') + { + return false; + } + else if (n > UINT_MAX) + { + return false; + } + + *number = n; + + return true; +} + + +/* + * converts given string to 64 bit unsigned integer value. + * returns 0 upon failure and sets error flag + */ +bool +stringToUInt64(const char *str, uint64_t *number) +{ + char *endptr; + + if (str == NULL) + { + return false; + } + + if (number == NULL) + { + return false; + } + + errno = 0; + unsigned long long n = strtoull(str, &endptr, 10); + + if (str == endptr) + { + return false; + } + else if (errno != 0) + { + return false; + } + else if (*endptr != '\0') + { + return false; + } + else if (n > UINT64_MAX) + { + return false; + } + + *number = n; + + return true; +} + + +/* + * converts given string to short value. + * returns 0 upon failure and sets error flag + */ +bool +stringToShort(const char *str, short *number) +{ + char *endptr; + + if (str == NULL) + { + return false; + } + + if (number == NULL) + { + return false; + } + + errno = 0; + + long long int n = strtoll(str, &endptr, 10); + + if (str == endptr) + { + return false; + } + else if (errno != 0) + { + return false; + } + else if (*endptr != '\0') + { + return false; + } + else if (n < SHRT_MIN || n > SHRT_MAX) + { + return false; + } + + *number = n; + + return true; +} + + +/* + * converts given string to unsigned short value. + * returns 0 upon failure and sets error flag + */ +bool +stringToUShort(const char *str, unsigned short *number) +{ + char *endptr; + + if (str == NULL) + { + return false; + } + + if (number == NULL) + { + return false; + } + + errno = 0; + unsigned long long n = strtoull(str, &endptr, 10); + + if (str == endptr) + { + return false; + } + else if (errno != 0) + { + return false; + } + else if (*endptr != '\0') + { + return false; + } + else if (n > USHRT_MAX) + { + return false; + } + + *number = n; + + return true; +} + + +/* + * converts given string to 32 bit integer value. + * returns 0 upon failure and sets error flag + */ +bool +stringToInt32(const char *str, int32_t *number) +{ + char *endptr; + + if (str == NULL) + { + return false; + } + + if (number == NULL) + { + return false; + } + + errno = 0; + + long long int n = strtoll(str, &endptr, 10); + + if (str == endptr) + { + return false; + } + else if (errno != 0) + { + return false; + } + else if (*endptr != '\0') + { + return false; + } + else if (n < INT32_MIN || n > INT32_MAX) + { + return false; + } + + *number = n; + + return true; +} + + +/* + * converts given string to 32 bit unsigned int value. + * returns 0 upon failure and sets error flag + */ +bool +stringToUInt32(const char *str, uint32_t *number) +{ + char *endptr; + + if (str == NULL) + { + return false; + } + + if (number == NULL) + { + return false; + } + + errno = 0; + unsigned long long n = strtoull(str, &endptr, 10); + + if (str == endptr) + { + return false; + } + else if (errno != 0) + { + return false; + } + else if (*endptr != '\0') + { + return false; + } + else if (n > UINT32_MAX) + { + return false; + } + + *number = n; + + return true; +} + + +/* + * converts given string to a double precision float value. + * returns 0 upon failure and sets error flag + */ +bool +stringToDouble(const char *str, double *number) +{ + char *endptr; + + if (str == NULL) + { + return false; + } + + if (number == NULL) + { + return false; + } + + errno = 0; + double n = strtod(str, &endptr); + + if (str == endptr) + { + return false; + } + else if (errno != 0) + { + return false; + } + else if (*endptr != '\0') + { + return false; + } + else if (n > DBL_MAX) + { + return false; + } + + *number = n; + + return true; +} + + +/* + * IntervalToString prepares a string buffer to represent a given interval + * value given as a double precision float number. + */ +bool +IntervalToString(double seconds, char *buffer, size_t size) +{ + if (seconds < 1.0) + { + /* when we have < 1s, we round to 1s */ + sformat(buffer, size, " %ds", 1); + } + else if (seconds < 60.0) + { + int s = (int) seconds; + + sformat(buffer, size, "%2ds", s); + } + else if (seconds < (60.0 * 60.0)) + { + int mins = (int) (seconds / 60.0); + int secs = (int) (seconds - (mins * 60.0)); + + sformat(buffer, size, "%2dm%02ds", mins, secs); + } + else if (seconds < (24.0 * 60.0 * 60.0)) + { + int hours = (int) (seconds / (60.0 * 60.0)); + int mins = (int) ((seconds - (hours * 60.0 * 60.0)) / 60.0); + + sformat(buffer, size, "%2dh%02dm", hours, mins); + } + else + { + int days = (int) (seconds / (24.0 * 60.0 * 60.0)); + int hours = + (int) ((seconds - (days * 24.0 * 60.0 * 60.0)) / (60.0 * 60.0)); + + sformat(buffer, size, "%2dd%02dh", days, hours); + } + + return true; +} + + +/* + * countLines returns how many line separators (\n) are found in the given + * string. + */ +int +countLines(char *buffer) +{ + int lineNumber = 0; + char *currentLine = buffer; + + if (buffer == NULL) + { + return 0; + } + + do { + char *newLinePtr = strchr(currentLine, '\n'); + + if (newLinePtr == NULL) + { + if (strlen(currentLine) > 0) + { + ++lineNumber; + } + currentLine = NULL; + } + else + { + ++lineNumber; + currentLine = ++newLinePtr; + } + } while (currentLine != NULL && *currentLine != '\0'); + + return lineNumber; +} + + +/* + * splitLines prepares a multi-line error message in a way that calling code + * can loop around one line at a time and call log_error() or log_warn() on + * individual lines. + */ +int +splitLines(char *errorMessage, char **linesArray, int size) +{ + int lineNumber = 0; + char *currentLine = errorMessage; + + if (errorMessage == NULL) + { + return 0; + } + + do { + char *newLinePtr = strchr(currentLine, '\n'); + + if (newLinePtr == NULL) + { + if (strlen(currentLine) > 0) + { + linesArray[lineNumber++] = currentLine; + } + + currentLine = NULL; + } + else + { + *newLinePtr = '\0'; + + linesArray[lineNumber++] = currentLine; + + currentLine = ++newLinePtr; + } + } while (currentLine != NULL && *currentLine != '\0' && lineNumber < size); + + return lineNumber; +} + + +/* + * processBufferCallback is a function callback to use with the subcommands.c + * library when we want to output a command's output as it's running, such as + * when running a pg_basebackup command. + */ +void +processBufferCallback(const char *buffer, bool error) +{ + char *outLines[BUFSIZE] = { 0 }; + int lineCount = splitLines((char *) buffer, outLines, BUFSIZE); + int lineNumber = 0; + + for (lineNumber = 0; lineNumber < lineCount; lineNumber++) + { + if (strneq(outLines[lineNumber], "")) + { + /* + * pg_basebackup and other utilities write their progress output on + * stderr, we don't want to have ERROR message when it's all good. + * As a result we always target INFO log level here. + */ + log_info("%s", outLines[lineNumber]); + } + } +} diff --git a/src/bin/common/string_utils.h b/src/bin/common/string_utils.h new file mode 100644 index 000000000..928e6defb --- /dev/null +++ b/src/bin/common/string_utils.h @@ -0,0 +1,44 @@ +/* + * src/bin/pg_autoctl/string_utils.h + * Utility functions for string handling + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the PostgreSQL License. + * + */ +#ifndef STRING_UTILS_H +#define STRING_UTILS_H + +#include + + +/* maximum decimal int64 length with minus and NUL */ +#define INTSTRING_MAX_DIGITS 21 +typedef struct IntString +{ + int64_t intValue; + char strValue[INTSTRING_MAX_DIGITS]; +} IntString; + +IntString intToString(int64_t number); + +bool stringToInt(const char *str, int *number); +bool stringToUInt(const char *str, unsigned int *number); + +bool stringToInt64(const char *str, int64_t *number); +bool stringToUInt64(const char *str, uint64_t *number); + +bool stringToShort(const char *str, short *number); +bool stringToUShort(const char *str, unsigned short *number); + +bool stringToInt32(const char *str, int32_t *number); +bool stringToUInt32(const char *str, uint32_t *number); + +bool stringToDouble(const char *str, double *number); +bool IntervalToString(double seconds, char *buffer, size_t size); + +int countLines(char *buffer); +int splitLines(char *errorMessage, char **linesArray, int size); +void processBufferCallback(const char *buffer, bool error); + +#endif /* STRING_UTILS_h */ diff --git a/src/bin/common/system_utils.c b/src/bin/common/system_utils.c new file mode 100644 index 000000000..c19dfa10b --- /dev/null +++ b/src/bin/common/system_utils.c @@ -0,0 +1,145 @@ +/* + * src/bin/pg_autoctl/hardware_utils.c + * Utility functions for getting CPU and Memory information. + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the PostgreSQL License. + * + */ + +#if defined(__linux__) +#include +#else +#include +#include +#include +#endif + +#include + +#include "log.h" +#include "file_utils.h" +#include "system_utils.h" + +#if defined(__linux__) +static bool get_system_info_linux(SystemInfo *sysInfo); +#endif + +#if defined(__APPLE__) || defined(BSD) +static bool get_system_info_bsd(SystemInfo *sysInfo); +#endif + +/* + * get_system_info probes for system information and fills the given SystemInfo + * structure with what we found: number of CPUs and total amount of memory. + */ +bool +get_system_info(SystemInfo *sysInfo) +{ +#if defined(__APPLE__) || defined(BSD) + return get_system_info_bsd(sysInfo); +#elif defined(__linux__) + return get_system_info_linux(sysInfo); +#else + log_error("Failed to get system information: " + "Operating System not supported"); + return false; +#endif +} + + +/* + * On Linux, use sysinfo(2) and getnprocs(3) + */ +#if defined(__linux__) +static bool +get_system_info_linux(SystemInfo *sysInfo) +{ + struct sysinfo linuxSysInfo = { 0 }; + + if (sysinfo(&linuxSysInfo) != 0) + { + log_error("Failed to call sysinfo(): %m"); + return false; + } + + sysInfo->ncpu = get_nprocs(); + sysInfo->totalram = linuxSysInfo.totalram; + + return true; +} + + +#endif + + +/* + * FreeBSD, OpenBSD, and darwin use the sysctl(3) API. + */ +#if defined(__APPLE__) || defined(BSD) +static bool +get_system_info_bsd(SystemInfo *sysInfo) +{ + unsigned int ncpu = 0; /* the API requires an integer here */ + int ncpuMIB[2] = { CTL_HW, HW_NCPU }; + #if defined(HW_MEMSIZE) + int ramMIB[2] = { CTL_HW, HW_MEMSIZE }; /* MacOS */ + #elif defined(HW_PHYSMEM64) + int ramMIB[2] = { CTL_HW, HW_PHYSMEM64 }; /* OpenBSD */ + #else + int ramMIB[2] = { CTL_HW, HW_PHYSMEM }; /* FreeBSD */ + #endif + + size_t cpuSize = sizeof(ncpu); + size_t memSize = sizeof(sysInfo->totalram); + + if (sysctl(ncpuMIB, 2, &ncpu, &cpuSize, NULL, 0) == -1) + { + log_error("Failed to probe number of CPUs: %m"); + return false; + } + + sysInfo->ncpu = (unsigned short) ncpu; + + if (sysctl(ramMIB, 2, &(sysInfo->totalram), &memSize, NULL, 0) == -1) + { + log_error("Failed to probe Physical Memory: %m"); + return false; + } + + return true; +} + + +#endif + + +/* + * pretty_print_bytes pretty prints bytes in a human readable form. Given + * 17179869184 it places the string "16 GB" in the given buffer. + */ +void +pretty_print_bytes(char *buffer, size_t size, uint64_t bytes) +{ + const char *suffixes[7] = { + "B", /* Bytes */ + "kB", /* Kilo */ + "MB", /* Mega */ + "GB", /* Giga */ + "TB", /* Tera */ + "PB", /* Peta */ + "EB" /* Exa */ + }; + + uint sIndex = 0; + long double count = bytes; + + while (count >= 10240 && sIndex < 7) + { + sIndex++; + count /= 1024; + } + + /* forget about having more precision, Postgres wants integers here */ + sformat(buffer, size, "%d %s", (int) count, suffixes[sIndex]); +} diff --git a/src/bin/common/system_utils.h b/src/bin/common/system_utils.h new file mode 100644 index 000000000..64dd2fa1d --- /dev/null +++ b/src/bin/common/system_utils.h @@ -0,0 +1,27 @@ +/* + * src/bin/pg_autoctl/system_utils.h + * Utility functions for getting CPU and Memory information. + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the PostgreSQL License. + * + */ + +#ifndef SYSTEM_UTILS_H +#define SYSTEM_UTILS_H + +#include + + +/* taken from sysinfo(2) on Linux */ +typedef struct SystemInfo +{ + uint64_t totalram; /* Total usable main memory size */ + unsigned short ncpu; /* Number of current processes */ +} SystemInfo; + +bool get_system_info(SystemInfo *sysInfo); +void pretty_print_bytes(char *buffer, size_t size, uint64_t bytes); + + +#endif /* SYSTEM_UTILS_H */ diff --git a/src/bin/pg_autoctl/Makefile b/src/bin/pg_autoctl/Makefile index f1d884db1..b1bc05322 100644 --- a/src/bin/pg_autoctl/Makefile +++ b/src/bin/pg_autoctl/Makefile @@ -5,104 +5,37 @@ PG_AUTOCTL = ./pg_autoctl SRC_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST)))) -DEPDIR = $(SRC_DIR)/.deps +# Must be set before include so that targets in Makefile.common don't +# become the default goal when included before the all: rule below. +.DEFAULT_GOAL := all -INCLUDES = $(patsubst ${SRC_DIR}%.h,%.h,$(wildcard ${SRC_DIR}*.h)) +include $(SRC_DIR)../common/Makefile.common -SRC = $(patsubst ${SRC_DIR}%.c,%.c,$(wildcard ${SRC_DIR}*.c)) -OBJS = $(patsubst %.c,%.o,$(SRC)) -OBJS += lib-log.o lib-commandline.o lib-parson.o lib-snprintf.o lib-strerror.o - -PG_CONFIG ?= pg_config -BINDIR ?= $(shell $(PG_CONFIG) --bindir) - -PG_SNPRINTF = $(wildcard ${SRC_DIR}../lib/pg/snprintf.*) -LOG_SRC = $(wildcard ${SRC_DIR}../lib/log/src/log.*) -COMMANDLINE_SRC = $(wildcard ${SRC_DIR}../lib/subcommands.c/commandline.*) -PARSON_SRC = $(wildcard ${SRC_DIR}../lib/parson/parson.*) - -COMMON_LIBS = -I${SRC_DIR}../lib/pg -COMMON_LIBS += -I${SRC_DIR}../lib/log/src/ -COMMON_LIBS += -I${SRC_DIR}../lib/subcommands.c/ -COMMON_LIBS += -I${SRC_DIR}../lib/libs/ -COMMON_LIBS += -I${SRC_DIR}../lib/parson/ - -CC = $(shell $(PG_CONFIG) --cc) +# pg_autoctl includes its own source directory too +override CFLAGS += -I$(SRC_DIR) -DEFAULT_CFLAGS = -std=c99 -D_GNU_SOURCE -g -DEFAULT_CFLAGS += -I $(shell $(PG_CONFIG) --includedir) -DEFAULT_CFLAGS += -I $(shell $(PG_CONFIG) --includedir-server) -DEFAULT_CFLAGS += -I $(shell $(PG_CONFIG) --pkgincludedir)/internal -DEFAULT_CFLAGS += $(shell $(PG_CONFIG) --cflags) -DEFAULT_CFLAGS += -Wformat -DEFAULT_CFLAGS += -Wall -DEFAULT_CFLAGS += -Werror=implicit-int -DEFAULT_CFLAGS += -Werror=implicit-function-declaration -DEFAULT_CFLAGS += -Werror=return-type -DEFAULT_CFLAGS += -Wno-declaration-after-statement +INCLUDES = $(patsubst $(SRC_DIR)%.h,%.h,$(wildcard $(SRC_DIR)*.h)) -# Needed for FreeBSD -DEFAULT_CFLAGS += -D_WANT_SEMUN - -# Needed for OSX -DEFAULT_CFLAGS += -Wno-missing-braces -DEFAULT_CFLAGS += $(COMMON_LIBS) - -ifdef USE_SECURITY_FLAGS -# Flags taken from: https://liquid.microsoft.com/Web/Object/Read/ms.security/Requirements/Microsoft.Security.SystemsADM.10203#guide -SECURITY_CFLAGS=-fstack-protector-strong -D_FORTIFY_SOURCE=2 -O2 -z noexecstack -fpie -Wl,-pie -Wl,-z,relro -Wl,-z,now -Wformat -Wformat-security -Werror=format-security -DEFAULT_CFLAGS += $(SECURITY_CFLAGS) -endif - -override CFLAGS := $(DEFAULT_CFLAGS) $(CFLAGS) +SRC = $(patsubst $(SRC_DIR)%.c,%.c,$(wildcard $(SRC_DIR)*.c)) +OBJS = $(patsubst %.c,%.o,$(SRC)) +OBJS += lib-log.o lib-commandline.o lib-parson.o lib-snprintf.o lib-strerror.o +OBJS += $(COMMON_LIB) -LIBS = -L $(shell $(PG_CONFIG) --pkglibdir) -LIBS += -L $(shell $(PG_CONFIG) --libdir) -LIBS += $(shell $(PG_CONFIG) --ldflags) -LIBS += $(shell $(PG_CONFIG) --libs) -LIBS += -lpq +# ncurses needed for pg_autoctl watch LIBS += -lncurses -all: $(PG_AUTOCTL) ; - -# Based on Postgres Makefile for automatic dependency generation -# https://github.com/postgres/postgres/blob/1933ae629e7b706c6c23673a381e778819db307d/src/Makefile.global.in#L890-L924 -%.o : %.c - @if test ! -d $(DEPDIR); then mkdir -p $(DEPDIR); fi - $(CC) $(CFLAGS) -c -MMD -MP -MF$(DEPDIR)/$(*F).Po -o $@ $< - -Po_files := $(wildcard $(DEPDIR)/*.Po) -ifneq (,$(Po_files)) -include $(Po_files) -endif - +all: $(COMMON_LIB) $(PG_AUTOCTL) ; $(PG_AUTOCTL): $(OBJS) $(INCLUDES) $(CC) $(CFLAGS) $(OBJS) $(LDFLAGS) $(LIBS) -o $@ -lib-snprintf.o: $(PG_SNPRINTF) - $(CC) $(CFLAGS) -c -MMD -MP -MF$(DEPDIR)/$(*F).Po -MT$@ -o $@ ${SRC_DIR}../lib/pg/snprintf.c - -lib-strerror.o: $(PG_SNPRINTF) - $(CC) $(CFLAGS) -c -MMD -MP -MF$(DEPDIR)/$(*F).Po -MT$@ -o $@ ${SRC_DIR}../lib/pg/strerror.c - -lib-log.o: $(LOG_SRC) - $(CC) $(CFLAGS) -c -MMD -MP -MF$(DEPDIR)/$(*F).Po -MT$@ -o $@ ${SRC_DIR}../lib/log/src/log.c - -lib-commandline.o: $(COMMANDLINE_SRC) - $(CC) $(CFLAGS) -c -MMD -MP -MF$(DEPDIR)/$(*F).Po -MT$@ -o $@ ${SRC_DIR}../lib/subcommands.c/commandline.c - -lib-parson.o: $(PARSON_SRC) - $(CC) $(CFLAGS) -c -MMD -MP -MF$(DEPDIR)/$(*F).Po -MT$@ -o $@ ${SRC_DIR}../lib/parson/parson.c - clean: rm -f $(OBJS) $(PG_AUTOCTL) + rm -f $(COMMON_LIB) $(COMMON_OBJ) rm -rf $(DEPDIR) install: $(PG_AUTOCTL) install -d $(DESTDIR)$(BINDIR) install -m 0755 $(PG_AUTOCTL) $(DESTDIR)$(BINDIR) - - -.PHONY: all monitor clean +.PHONY: all clean install diff --git a/src/bin/pgaftest/Makefile b/src/bin/pgaftest/Makefile new file mode 100644 index 000000000..259b77a89 --- /dev/null +++ b/src/bin/pgaftest/Makefile @@ -0,0 +1,120 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the PostgreSQL License. + +PGAFTEST = ./pgaftest + +SRC_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST)))) +PGACTL_DIR := $(SRC_DIR)../pg_autoctl/ + +# Must be set before include so that targets in Makefile.common don't +# become the default goal when included before the all: rule below. +.DEFAULT_GOAL := all + +include $(SRC_DIR)../common/Makefile.common + +# Include both our own dir and pg_autoctl for shared headers +override CFLAGS += -I$(SRC_DIR) -I$(PGACTL_DIR) + +# ----------------------------------------------------------------------- +# Sources that live in this directory +# ----------------------------------------------------------------------- +LOCAL_SRC = main.c cli_root.c cli_demo.c cli_indent.c \ + compose_gen.c test_runner.c \ + test_spec_scan.c test_spec_parse.c + +LOCAL_OBJS = $(patsubst %.c,%.o,$(LOCAL_SRC)) + +# ----------------------------------------------------------------------- +# pg_autoctl-specific shared sources (business logic; not in common/). +# Compiled as shared-%.o to distinguish from local objects. +# ----------------------------------------------------------------------- +SHARED_SRCS = cli_common.c config.c coordinator.c fsm.c fsm_transition.c \ + fsm_transition_citus.c keeper.c keeper_config.c keeper_pg_init.c \ + monitor.c monitor_config.c monitor_pg_init.c \ + nodespec.c nodestate_utils.c pghba.c primary_standby.c \ + service_keeper.c service_keeper_init.c service_monitor.c \ + service_monitor_init.c service_postgres.c service_postgres_ctl.c \ + state.c supervisor.c systemd_config.c + +SHARED_OBJS = $(patsubst %.c,shared-%.o,$(SHARED_SRCS)) + +OBJS = $(LOCAL_OBJS) $(SHARED_OBJS) +OBJS += lib-log.o lib-commandline.o lib-parson.o lib-snprintf.o lib-strerror.o +OBJS += $(COMMON_LIB) + +INCLUDES = $(wildcard $(SRC_DIR)*.h) + +# ----------------------------------------------------------------------- +# Flex / Bison +# ----------------------------------------------------------------------- +FLEX = flex +BISON = bison + +test_spec_scan.c: test_spec_scan.l test_spec_parse.h + $(FLEX) -o $@ $< + +test_spec_parse.c test_spec_parse.h: test_spec_parse.y + $(BISON) -d -o test_spec_parse.c $< + +# Convenience target to regenerate parser/scanner sources explicitly. +# Running "make generate" is the only supported way to regenerate the +# committed .c/.h files from the .y/.l grammars. +.PHONY: generate +generate: test_spec_parse.c test_spec_parse.h test_spec_scan.c + +# cli_indent.c includes test_spec_parse.h (bison-generated); ensure it's +# built first so a parallel clean build doesn't race on the generated header. +cli_indent.o: test_spec_parse.h + +# Bison/flex generated files have unavoidable unused tables and helper +# functions that vary by generator version. Suppress those warnings so +# that pg_config --cflags -Werror does not break the build. +GENERATED_CFLAGS = $(CFLAGS) \ + -Wno-unused-variable \ + -Wno-unused-function \ + -Wno-unused-const-variable \ + -Wno-return-type \ + -Wno-error + +test_spec_parse.o: test_spec_parse.c + @if test ! -d $(DEPDIR); then mkdir -p $(DEPDIR); fi + $(CC) $(GENERATED_CFLAGS) -c -MMD -MP -MF$(DEPDIR)/test_spec_parse.Po -o $@ $< + +test_spec_scan.o: test_spec_scan.c + @if test ! -d $(DEPDIR); then mkdir -p $(DEPDIR); fi + $(CC) $(GENERATED_CFLAGS) -c -MMD -MP -MF$(DEPDIR)/test_spec_scan.Po -o $@ $< + +# ----------------------------------------------------------------------- +# Rules for shared objects (compiled from pg_autoctl/ sources) +# ----------------------------------------------------------------------- +shared-%.o: $(PGACTL_DIR)%.c + @if test ! -d $(DEPDIR); then mkdir -p $(DEPDIR); fi + $(CC) $(CFLAGS) -c -MMD -MP -MF$(DEPDIR)/shared-$(*F).Po -o $@ $< + +Po_files := $(wildcard $(DEPDIR)/*.Po) +ifneq (,$(Po_files)) +include $(Po_files) +endif + +# ----------------------------------------------------------------------- +all: $(COMMON_LIB) $(PGAFTEST) ; + +$(PGAFTEST): $(OBJS) $(INCLUDES) + $(CC) $(CFLAGS) $(OBJS) $(LDFLAGS) $(LIBS) -o $@ + +clean: + rm -f $(OBJS) $(PGAFTEST) + rm -f $(COMMON_LIB) $(COMMON_OBJ) + rm -rf $(DEPDIR) + +# Remove generated parser/scanner sources only when explicitly requested. +# The committed .c files allow building without bison/flex installed and +# without risking version-skew in Docker. +distclean: clean + rm -f test_spec_scan.c test_spec_parse.c test_spec_parse.h + +install: $(PGAFTEST) + install -d $(DESTDIR)$(BINDIR) + install -m 0755 $(PGAFTEST) $(DESTDIR)$(BINDIR) + +.PHONY: all clean install diff --git a/src/bin/pgaftest/cli_demo.c b/src/bin/pgaftest/cli_demo.c new file mode 100644 index 000000000..8193b8b71 --- /dev/null +++ b/src/bin/pgaftest/cli_demo.c @@ -0,0 +1,69 @@ +/* + * src/bin/pgaftest/cli_demo.c + * Demo application sub-command for pgaftest (moved from pg_autoctl do demo). + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the PostgreSQL License. + */ + +#include +#include "commandline.h" +#include "cli_demo.h" +#include "log.h" + +/* + * The demo sub-commands forward to pg_autoctl's demoapp implementation. + * For now we provide a stub that guides the user; the full implementation + * is linked from the shared pg_autoctl sources. + */ +static void +cli_demo_run(int argc, char **argv) +{ + log_error("pgaftest demo run: not yet implemented in this build."); + log_info("Use: docker compose exec pg_autoctl do demo run ..."); +} + + +static void +cli_demo_uri(int argc, char **argv) +{ + log_error("pgaftest demo uri: not yet implemented in this build."); +} + + +static void +cli_demo_ping(int argc, char **argv) +{ + log_error("pgaftest demo ping: not yet implemented in this build."); +} + + +static void +cli_demo_summary(int argc, char **argv) +{ + log_error("pgaftest demo summary: not yet implemented in this build."); +} + + +static CommandLine demo_run = + make_command("run", "Run the demo app", "", "", NULL, cli_demo_run); +static CommandLine demo_uri = + make_command("uri", "Show app URI", "", "", NULL, cli_demo_uri); +static CommandLine demo_ping = + make_command("ping", "Ping the app URI", "", "", NULL, cli_demo_ping); +static CommandLine demo_summary = + make_command("summary", "Show demo summary", "", "", NULL, cli_demo_summary); + +static CommandLine *demo_subcommands[] = { + &demo_run, + &demo_uri, + &demo_ping, + &demo_summary, + NULL +}; + +CommandLine pgaftest_demo_command = + make_command_set("demo", + "Demo application for pg_auto_failover", + "", "", + NULL, demo_subcommands); diff --git a/src/bin/pgaftest/cli_demo.h b/src/bin/pgaftest/cli_demo.h new file mode 100644 index 000000000..8c11bc470 --- /dev/null +++ b/src/bin/pgaftest/cli_demo.h @@ -0,0 +1,16 @@ +/* + * src/bin/pgaftest/cli_demo.h + * Demo application CLI (moved from pg_autoctl do demo). + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the PostgreSQL License. + */ + +#ifndef CLI_DEMO_H +#define CLI_DEMO_H + +#include "commandline.h" + +extern CommandLine pgaftest_demo_command; + +#endif /* CLI_DEMO_H */ diff --git a/src/bin/pgaftest/cli_indent.c b/src/bin/pgaftest/cli_indent.c new file mode 100644 index 000000000..ce4a79ae2 --- /dev/null +++ b/src/bin/pgaftest/cli_indent.c @@ -0,0 +1,1149 @@ +/* + * src/bin/pgaftest/cli_indent.c + * pgaftest indent — parse a .pgaf file and rewrite it with canonical + * indentation. + * + * The output format follows these conventions: + * - 4-space indentation for all block contents + * - multi-line node specs in cluster{}: each property on its own continuation + * line, indented to align under the node name + * - multi-condition wait until: each "and state is " on its own + * line, indented under the first condition + * - sequence block: one step name per line, indented + * - leading file comment block is preserved verbatim + * - comment blocks immediately before setup/teardown/step are preserved + * - inline comments inside step bodies are dropped (AST does not store them) + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the PostgreSQL License. + */ + +#include +#include +#include + +#include "cli_indent.h" +#include "test_spec.h" +#include "test_spec_parse.h" +#include "defaults.h" +#include "log.h" +#include "string_utils.h" + +/* + * StepComment — comment block that precedes a named block in the raw file. + * name is "setup", "teardown", or the step name. + * text is malloc'd; NULL if no comment precedes that block. + */ +#define MAX_STEP_COMMENTS 256 + +typedef struct +{ + char name[128]; + char *text; +} StepComment; + +typedef struct +{ + StepComment entries[MAX_STEP_COMMENTS]; + int count; +} StepCommentMap; + +/* forward declarations */ +static void print_header(FILE *out, const char *path); +static StepCommentMap * collect_comments(const char *path); +static const char * lookup_comment(const StepCommentMap *m, const char *name); +static void free_comments(StepCommentMap *m); +static void print_cluster(FILE *out, const TestSpec *spec); +static void print_step(FILE *out, const TestStep *step, bool named); +static void print_cmd(FILE *out, const TestCmd *cmd, int indent); +static void print_cmd_wait(FILE *out, const TestCmd *cmd, int indent); +static void normalize_sql(const char *in, char *out, int outlen); +static int match_clause_keyword(const char *p); +static void emit_segment(FILE *out, const char *seg, int bodyIndent); +static void print_sql_body(FILE *out, const char *raw_sql, int bodyIndent); + +/* ----------------------------------------------------------------------- + * Entry point called from cli_root.c + * ----------------------------------------------------------------------- */ +void +cli_indent(int argc, char **argv) +{ + if (argc < 1 || argv[0] == NULL) + { + fprintf(stderr, "Usage: pgaftest indent \n"); + exit(EXIT_CODE_BAD_ARGS); + } + + const char *path = argv[0]; + + TestSpec *spec = parse_test_spec(path); + if (!spec) + { + log_error("Failed to parse \"%s\"", path); + exit(EXIT_CODE_BAD_ARGS); + } + + StepCommentMap *cmap = collect_comments(path); + + FILE *out = stdout; + + print_header(out, path); + + print_cluster(out, spec); + + if (spec->setup) + { + const char *cmt = lookup_comment(cmap, "setup"); + if (cmt) + { + fprintf(out, "\n%s\nsetup {\n", cmt); + } + else + { + fprintf(out, "\nsetup {\n"); + } + print_step(out, spec->setup, false); + fprintf(out, "}\n"); + } + + if (spec->teardown) + { + const char *cmt = lookup_comment(cmap, "teardown"); + if (cmt) + { + fprintf(out, "\n%s\nteardown {\n", cmt); + } + else + { + fprintf(out, "\nteardown {\n"); + } + print_step(out, spec->teardown, false); + fprintf(out, "}\n"); + } + + for (TestStep *s = spec->steps; s; s = s->next) + { + const char *cmt = lookup_comment(cmap, s->name); + if (cmt) + { + fprintf(out, "\n%s\nstep %s {\n", cmt, s->name); + } + else + { + fprintf(out, "\nstep %s {\n", s->name); + } + print_step(out, s, true); + fprintf(out, "}\n"); + } + + /* + * Emit the sequence block only when it differs from definition order. + * When sequence == steps-in-order the block is redundant noise; drop it. + */ + if (spec->sequenceLength > 0) + { + /* Check whether sequence matches step definition order exactly */ + bool redundant = true; + int si = 0; + for (TestStep *s = spec->steps; s && si < spec->sequenceLength; s = s->next, si++) + { + if (strcmp(spec->sequence[si], s->name) != 0) + { + redundant = false; + break; + } + } + + /* Also redundant only if counts match */ + if (si != spec->sequenceLength || spec->sequenceLength != spec->stepCount) + { + redundant = false; + } + + if (!redundant) + { + fprintf(out, "\nsequence\n"); + for (int i = 0; i < spec->sequenceLength; i++) + { + fprintf(out, " %s\n", spec->sequence[i]); + } + } + } + + free_comments(cmap); + exit(0); +} + + +/* ----------------------------------------------------------------------- + * Header: emit the leading # comment block from the original file. + * The parser drops comments, so we re-read the raw file here. + * Falls back to "# " when the file has no leading comment. + * ----------------------------------------------------------------------- */ +static void +print_header(FILE *out, const char *path) +{ + FILE *f = fopen(path, "r"); + if (!f) + { + fprintf(out, "# %s\n\n", path); + return; + } + + char line[1024]; + bool any = false; + + char pending_blank[1024] = ""; /* blank lines between comment blocks */ + + while (fgets(line, sizeof(line), f)) + { + /* stop at first non-comment, non-blank line */ + if (line[0] != '#' && line[0] != '\n' && line[0] != '\r') + { + break; + } + + if (line[0] == '\n' || line[0] == '\r') + { + if (any) + { + /* accumulate blank lines; flush only if more comments follow */ + strncat(pending_blank, line, + sizeof(pending_blank) - strlen(pending_blank) - 1); + } + continue; + } + + /* comment line: flush any accumulated blanks first */ + if (pending_blank[0]) + { + fputs(pending_blank, out); + pending_blank[0] = '\0'; + } + fputs(line, out); + any = true; + } + + /* discard trailing blank lines — add a single blank after */ + fclose(f); + + if (!any) + { + fprintf(out, "# %s\n", path); + } + + fprintf(out, "\n"); +} + + +/* ----------------------------------------------------------------------- + * Step comment collection — scan the raw file for # blocks that + * immediately precede "step {", "setup {", or "teardown {". + * ----------------------------------------------------------------------- */ +static StepCommentMap * +collect_comments(const char *path) +{ + StepCommentMap *m = calloc(1, sizeof(StepCommentMap)); + if (!m) + { + return m; + } + + FILE *f = fopen(path, "r"); + if (!f) + { + return m; + } + + char line[1024]; + char pending[8192] = ""; /* accumulated # lines not yet assigned */ + bool skip_file_header = true; + + while (fgets(line, sizeof(line), f)) + { + /* Skip the leading file-header comment block (consumed by print_header) */ + if (skip_file_header) + { + if (line[0] == '#' || line[0] == '\n' || line[0] == '\r') + { + continue; + } + skip_file_header = false; + } + + if (line[0] == '#') + { + /* Accumulate comment line */ + strncat(pending, line, sizeof(pending) - strlen(pending) - 1); + continue; + } + + if (line[0] == '\n' || line[0] == '\r') + { + /* Blank line: keep pending — comment may continue after blank */ + if (pending[0]) + { + strncat(pending, line, sizeof(pending) - strlen(pending) - 1); + } + continue; + } + + /* Check for a block keyword */ + char kw[128] = ""; + if (sscanf(line, "step %127s", kw) == 1) + { + /* strip trailing " {" from name */ + char *brace = strchr(kw, '{'); + if (brace) + { + *brace = '\0'; + } + } + else if (strncmp(line, "setup", 5) == 0) + { + strlcpy(kw, "setup", sizeof(kw)); + } + else if (strncmp(line, "teardown", 8) == 0) + { + strlcpy(kw, "teardown", sizeof(kw)); + } + + if (kw[0] && pending[0] && m->count < MAX_STEP_COMMENTS) + { + /* Strip leading blank lines */ + char *start = pending; + while (*start == '\n' || *start == '\r') + { + start++; + } + + /* Strip trailing blank lines; add exactly one trailing \n */ + int len = (int) strlen(start); + while (len > 0 && (start[len - 1] == '\n' || start[len - 1] == '\r' || + start[len - 1] == ' ')) + { + len--; + } + + if (len > 0) + { + char clean[8192]; + snprintf(clean, sizeof(clean), "%.*s\n", len, start); + strlcpy(m->entries[m->count].name, kw, + sizeof(m->entries[0].name)); + m->entries[m->count].text = strdup(clean); + m->count++; + } + } + + /* Reset pending regardless */ + pending[0] = '\0'; + } + + fclose(f); + return m; +} + + +static const char * +lookup_comment(const StepCommentMap *m, const char *name) +{ + if (!m) + { + return NULL; + } + for (int i = 0; i < m->count; i++) + { + if (strcmp(m->entries[i].name, name) == 0) + { + return m->entries[i].text; + } + } + return NULL; +} + + +static void +free_comments(StepCommentMap *m) +{ + if (!m) + { + return; + } + for (int i = 0; i < m->count; i++) + { + free(m->entries[i].text); + } + free(m); +} + + +/* ----------------------------------------------------------------------- + * Cluster block + * ----------------------------------------------------------------------- */ + +/* + * Node property token: a keyword and an optional value string (NULL = flag only). + */ +typedef struct +{ + const char *kw; + char val[128]; +} NodeProp; + +static void +print_node(FILE *out, const TestNode *n, int baseIndent) +{ + NodeProp props[32]; + int pc = 0; + char numbuf[32]; + + /* kind prefix — emitted inline after the name; for workers group is also inline */ + char kindbuf[64] = ""; + if (n->kind == NODE_KIND_CITUS_COORDINATOR) + { + strlcpy(kindbuf, "coordinator", sizeof(kindbuf)); + } + else if (n->kind == NODE_KIND_CITUS_WORKER) + { + if (n->group != 0) + { + snprintf(kindbuf, sizeof(kindbuf), "worker group %d", n->group); + } + else + { + strlcpy(kindbuf, "worker", sizeof(kindbuf)); + } + } + const char *kind = kindbuf; + +#define ADD(k, v) do { props[pc].kw = (k); strlcpy(props[pc].val, (v), \ + sizeof(props[0].val)); pc++; \ +} while (0) +#define ADDF(k) do { props[pc].kw = (k); props[pc].val[0] = '\0'; pc++; } while (0) + if (n->launchDeferred) + { + ADDF("launch deferred"); + } + if (n->async) + { + ADDF("async"); + } + if (n->noMonitor) + { + ADDF("no-monitor"); + } + if (n->listen) + { + ADDF("listen"); + } + if (n->candidatePriority != 50) + { + snprintf(numbuf, sizeof(numbuf), "%d", n->candidatePriority); + ADD("candidate-priority", numbuf); + } + if (!n->replicationQuorum) + { + ADD("replication-quorum", "false"); + } + if (n->citusSecondary) + { + ADDF("citus-secondary"); + } + if (n->citusClusterName[0]) + { + ADD("citus-cluster-name", n->citusClusterName); + } + if (n->pgPort != 0) + { + snprintf(numbuf, sizeof(numbuf), "%d", n->pgPort); + ADD("port", numbuf); + } + if (n->ssl[0]) + { + ADD("ssl", n->ssl); + } + if (n->auth[0]) + { + ADD("auth", n->auth); + } + if (n->debianCluster[0]) + { + ADD("debian-cluster", n->debianCluster); + } + + /* volumes are always multi-line; handled separately below */ + +#undef ADD + + /* compute inline representation */ + char inline_props[512] = ""; + for (int i = 0; i < pc; i++) + { + strncat(inline_props, " ", sizeof(inline_props) - strlen(inline_props) - 1); + strncat(inline_props, props[i].kw, sizeof(inline_props) - strlen(inline_props) - + 1); + if (props[i].val[0]) + { + strncat(inline_props, " ", sizeof(inline_props) - strlen(inline_props) - 1); + strncat(inline_props, props[i].val, sizeof(inline_props) - strlen( + inline_props) - 1); + } + } + + int nameLen = strlen(n->name); + int kindLen = kind[0] ? (int) strlen(kind) + 1 : 0; + int lineLen = baseIndent + nameLen + kindLen + strlen(inline_props); + + if ((lineLen <= 72 || pc == 0) && n->volumeCount == 0) + { + /* fits on one line — flat syntax, no "node" keyword needed */ + fprintf(out, "%*s%s", baseIndent, "", n->name); + if (kind[0]) + { + fprintf(out, " %s", kind); + } + fprintf(out, "%s\n", inline_props); + return; + } + + /* multi-line — use block syntax: node { \n kind\n opt\n opt\n } */ + int innerIndent = baseIndent + 4; + fprintf(out, "%*snode %s {\n", baseIndent, "", n->name); + if (kind[0]) + { + fprintf(out, "%*s%s\n", innerIndent, "", kind); + } + + for (int i = 0; i < pc; i++) + { + fprintf(out, "%*s%s", innerIndent, "", props[i].kw); + if (props[i].val[0]) + { + fprintf(out, " %s", props[i].val); + } + fprintf(out, "\n"); + } + for (int vi = 0; vi < n->volumeCount; vi++) + { + fprintf(out, "%*svolume %s \"%s\"\n", + innerIndent, "", + n->volumes[vi].name, n->volumes[vi].path); + } + fprintf(out, "%*s}\n", baseIndent, ""); +} + + +static void +print_cluster(FILE *out, const TestSpec *spec) +{ + const TestCluster *cl = &spec->cluster; + + fprintf(out, "cluster {\n"); + + if (cl->withMonitor) + { + fprintf(out, " monitor\n"); + } + + if (cl->secondMonitorName[0]) + { + fprintf(out, " monitor %s initially stopped\n", cl->secondMonitorName); + } + + if (cl->image[0]) + { + fprintf(out, " image \"%s\"\n", cl->image); + } + if (cl->ssl[0] && strcmp(cl->ssl, "self-signed") != 0) + { + fprintf(out, " ssl %s\n", cl->ssl); + } + if (cl->auth[0] && strcmp(cl->auth, "trust") != 0) + { + fprintf(out, " auth %s\n", cl->auth); + } + if (cl->extensionVersion[0]) + { + fprintf(out, " extension-version %s\n", cl->extensionVersion); + } + + for (int fi = 0; fi < cl->formationCount; fi++) + { + const TestFormation *f = &cl->formations[fi]; + + if (strcmp(f->name, "default") == 0) + { + fprintf(out, " formation {\n"); + } + else + { + fprintf(out, " formation %s {\n", f->name); + } + + for (int ni = 0; ni < f->nodeCount; ni++) + { + print_node(out, &f->nodes[ni], 8); + } + + fprintf(out, " }\n"); + } + + fprintf(out, "}\n"); +} + + +/* ----------------------------------------------------------------------- + * Step / command printing + * ----------------------------------------------------------------------- */ + +/* + * normalize_sql — collapse all whitespace runs (including embedded newlines + * from a multi-line block in the source file) to single spaces and trim + * leading/trailing whitespace. This makes `pgaftest indent` idempotent: a + * file that has already been indented produces the same output when indented + * again. + */ +static void +normalize_sql(const char *in, char *out, int outlen) +{ + const char *p = in; + char *q = out; + char *lim = out + outlen - 1; + bool ws = true; /* suppress leading whitespace */ + + while (*p && q < lim) + { + bool isws = (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r'); + if (isws) + { + if (!ws) + { + *q++ = ' '; + } + ws = true; + } + else + { + *q++ = *p; + ws = false; + } + p++; + } + + /* trim trailing space */ + if (q > out && q[-1] == ' ') + { + q--; + } + *q = '\0'; +} + + +/* + * SQL_CLAUSE_KEYWORDS — clause-level keywords that force a new output line. + * Two-word keywords (ORDER BY, GROUP BY) must appear before any single-word + * prefix (BY, ORDER, GROUP) to prevent partial matches. + */ +static const char *const SQL_CLAUSE_KEYWORDS[] = { + "ORDER BY", + "GROUP BY", + "FROM", + "WHERE", + "HAVING", + "LIMIT", + "OFFSET", + NULL +}; + +/* + * match_clause_keyword — if a SQL clause keyword starts at p (case- + * insensitive, followed by space, semicolon, or end-of-string), return its + * length; otherwise return 0. + */ +static int +match_clause_keyword(const char *p) +{ + for (int i = 0; SQL_CLAUSE_KEYWORDS[i]; i++) + { + const char *kw = SQL_CLAUSE_KEYWORDS[i]; + int len = (int) strlen(kw); + if (strncasecmp(p, kw, len) == 0) + { + char next = p[len]; + if (next == ' ' || next == '\0' || next == ';') + { + return len; + } + } + } + return 0; +} + + +/* + * emit_segment — word-wrap a single normalized segment (no embedded newlines, + * no leading/trailing spaces) onto one or more lines, each prefixed with + * bodyIndent spaces and at most 79 characters wide total. + */ +static void +emit_segment(FILE *out, const char *seg, int bodyIndent) +{ + int avail = 79 - bodyIndent; + if (avail < 20) + { + avail = 20; + } + + const char *p = seg; + + while (*p) + { + if ((int) strlen(p) <= avail) + { + fprintf(out, "%*s%s\n", bodyIndent, "", p); + break; + } + + /* Find the last space within avail columns. */ + const char *last_spc = NULL; + for (int i = 0; i < avail && p[i]; i++) + { + if (p[i] == ' ') + { + last_spc = p + i; + } + } + + if (last_spc) + { + fprintf(out, "%*s%.*s\n", + bodyIndent, "", (int) (last_spc - p), p); + p = last_spc + 1; /* skip the space */ + } + else + { + /* No space — hard break (avoids infinite loop on long tokens). */ + fprintf(out, "%*s%.*s\n", bodyIndent, "", avail, p); + p += avail; + } + } +} + + +/* + * print_sql_body — emit normalized SQL with clause-keyword-aware line + * splitting inside a sql { … } block. + * + * Algorithm: + * 1. Normalize whitespace (fixes idempotency when the input was already + * a multi-line block from a previous indent pass). + * 2. If the whole thing fits in avail columns, emit it on one line. + * 3. Otherwise, scan for clause keywords (FROM, WHERE, ORDER BY, …). + * Each keyword starts a new output line; within each segment, apply + * word-wrap at 79 columns. + */ +static void +print_sql_body(FILE *out, const char *raw_sql, int bodyIndent) +{ + char norm[8192]; + normalize_sql(raw_sql, norm, sizeof(norm)); + + int avail = 79 - bodyIndent; + if (avail < 20) + { + avail = 20; + } + + /* Short enough to fit on one line? */ + if ((int) strlen(norm) <= avail) + { + fprintf(out, "%*s%s\n", bodyIndent, "", norm); + return; + } + + /* + * Scan for clause-keyword split points. A keyword is a split point when + * it is preceded by a space (word boundary in the normalized string). + * We accumulate characters into a segment and flush when we hit a keyword. + */ + const char *p = norm; + const char *seg_start = norm; + + while (*p) + { + if (p > norm && p[-1] == ' ') + { + int kwlen = match_clause_keyword(p); + if (kwlen) + { + /* Flush the segment up to (but not including) the space + * that preceded this keyword. */ + int seglen = (int) ((p - 1) - seg_start); + if (seglen > 0) + { + char seg[8192]; + memcpy(seg, seg_start, (size_t) seglen); + seg[seglen] = '\0'; + emit_segment(out, seg, bodyIndent); + } + seg_start = p; /* keyword begins the next segment */ + } + } + p++; + } + + /* Flush the final segment. */ + if (*seg_start) + { + emit_segment(out, seg_start, bodyIndent); + } +} + + +static void +print_step(FILE *out, const TestStep *step, bool named) +{ + (void) named; + for (const TestCmd *cmd = step->commands; cmd; cmd = cmd->next) + { + print_cmd(out, cmd, 4); + } +} + + +static void +print_cmd(FILE *out, const TestCmd *cmd, int indent) +{ + switch (cmd->kind) + { + case CMD_EXEC: + { + fprintf(out, "%*sexec %s %s\n", indent, "", cmd->service, cmd->args); + break; + } + + case CMD_EXEC_FAILS: + { + fprintf(out, "%*sexec-fails %s %s\n", indent, "", + cmd->service, cmd->args); + break; + } + + case CMD_WAIT_STATE: + case CMD_ASSERT_STATE: + case CMD_ASSERT_ASSIGNED: + case CMD_WAIT_MULTI: + { + print_cmd_wait(out, cmd, indent); + break; + } + + case CMD_WAIT_STATES: + { + /* wait until s1, s2 [in group N] timeout Ns */ + fprintf(out, "%*swait until", indent, ""); + for (int i = 0; i < cmd->waitStateCount; i++) + { + if (i > 0) + { + fprintf(out, ","); + } + fprintf(out, " %s", cmd->waitStates[i]); + } + if (cmd->waitGroupCount > 0) + { + for (int g = 0; g < cmd->waitGroupCount; g++) + { + fprintf(out, " in group %d", cmd->waitGroups[g]); + } + } + fprintf(out, " timeout %ds\n", cmd->timeoutSeconds); + break; + } + + case CMD_WAIT_STOPPED: + { + fprintf(out, "%*swait until %s stopped timeout %ds\n", + indent, "", cmd->service, cmd->timeoutSeconds); + break; + } + + case CMD_PROMOTE: + { + fprintf(out, "%*spromote", indent, ""); + for (int i = 0; i < cmd->promoteCount; i++) + { + if (i > 0) + { + fprintf(out, ","); + } + fprintf(out, " %s", cmd->promoteNodes[i]); + } + fprintf(out, "\n"); + break; + } + + case CMD_SQL: + { + /* + * Normalise the SQL first (collapses embedded newlines from a + * previous multi-line block), then check if the whole thing fits + * on one line: " sql { }". + * If not, open a block and emit clause-keyword-split SQL. + */ + char norm[8192]; + normalize_sql(cmd->args, norm, sizeof(norm)); + + int oneliner_len = indent + + 4 + /* "sql " */ + (int) strlen(cmd->service) + + 3 + /* " { " */ + (int) strlen(norm) + + 2; /* " }" */ + + if (oneliner_len <= 79) + { + fprintf(out, "%*ssql %s { %s }\n", + indent, "", cmd->service, norm); + } + else + { + fprintf(out, "%*ssql %s {\n", indent, "", cmd->service); + print_sql_body(out, norm, indent + 4); + fprintf(out, "%*s}\n", indent, ""); + } + break; + } + + case CMD_EXPECT: + { + /* If the stored value has newlines it was converted from + * { r1 } { r2 } tuple syntax by expand_tuple_expect — reverse it. */ + if (strchr(cmd->expected, '\n')) + { + fprintf(out, "%*sexpect {", indent, ""); + const char *p = cmd->expected; + while (*p) + { + const char *nl = strchr(p, '\n'); + int len = nl ? (int) (nl - p) : (int) strlen(p); + fprintf(out, " { %.*s }", len, p); + p += len; + if (*p == '\n') + { + p++; + } + } + fprintf(out, " }\n"); + } + else + { + fprintf(out, "%*sexpect { %s }\n", indent, "", cmd->expected); + } + break; + } + + case CMD_EXPECT_ERROR: + { + fprintf(out, "%*sexpect error %s\n", indent, "", cmd->state); + break; + } + + case CMD_NETWORK_OFF: + { + fprintf(out, "%*snetwork disconnect %s\n", indent, "", cmd->service); + break; + } + + case CMD_NETWORK_ON: + { + fprintf(out, "%*snetwork connect %s\n", indent, "", cmd->service); + break; + } + + case CMD_SLEEP: + { + fprintf(out, "%*ssleep %ds\n", indent, "", cmd->timeoutSeconds); + break; + } + + case CMD_COMPOSE_DOWN: + { + fprintf(out, "%*scompose down\n", indent, ""); + break; + } + + case CMD_COMPOSE_START: + { + fprintf(out, "%*scompose start %s\n", indent, "", cmd->service); + break; + } + + case CMD_COMPOSE_STOP: + { + fprintf(out, "%*scompose stop %s\n", indent, "", cmd->service); + break; + } + + case CMD_COMPOSE_KILL: + { + fprintf(out, "%*scompose kill %s\n", indent, "", cmd->service); + break; + } + + case CMD_PG_AUTOCTL: + { + fprintf(out, "%*spg_autoctl %s\n", indent, "", cmd->args); + break; + } + + case CMD_STOP_POSTGRES: + { + fprintf(out, "%*sstop postgres %s\n", indent, "", cmd->service); + break; + } + + case CMD_START_POSTGRES: + { + fprintf(out, "%*sstart postgres %s\n", indent, "", cmd->service); + break; + } + + case CMD_STAYS_WHILE: + { + fprintf(out, "%*sassert %s stays %s while {\n", + indent, "", cmd->service, cmd->state); + for (const TestCmd *bc = cmd->body; bc; bc = bc->next) + { + print_cmd(out, bc, indent + 4); + } + fprintf(out, "%*s}\n", indent, ""); + break; + } + + case CMD_COMPOSE_INJECT: + { + fprintf(out, "%*scompose inject %s %s %s:%s\n", + indent, "", + cmd->expected, /* image */ + cmd->args, /* src path */ + cmd->service, /* dst service */ + cmd->state); /* dst path */ + break; + } + + case CMD_SET_MONITOR: + { + fprintf(out, "%*sset monitor %s\n", indent, "", cmd->service); + break; + } + + case CMD_LOGS_CHECK: + { + /* + * logs [not] contains|matches "" + * The logsNegate flag adds "not " and allowError picks the verb. + */ + const char *verb = cmd->allowError ? "matches" : "contains"; + fprintf(out, "%*slogs %s %s%s \"%s\"\n", + indent, "", + cmd->service, + cmd->logsNegate ? "not " : "", + verb, + cmd->args); + break; + } + + default: + { + fprintf(out, "%*s(unknown cmd %d)\n", indent, "", cmd->kind); + break; + } + } +} + + +/* + * print_cmd_wait handles the various wait/assert forms, emitting multi-line + * output for CMD_WAIT_MULTI and for single-node waits with pass-through. + */ +static void +print_cmd_wait(FILE *out, const TestCmd *cmd, int indent) +{ + if (cmd->kind == CMD_WAIT_MULTI) + { + /* first condition on same line, subsequent "and" lines indented */ + fprintf(out, "%*swait until %s state is %s\n", + indent, "", cmd->waitNodes[0], cmd->waitStates[0]); + for (int i = 1; i < cmd->waitStateCount; i++) + { + fprintf(out, "%*sand %s state is %s\n", + indent + 4, "", + cmd->waitNodes[i], cmd->waitStates[i]); + } + fprintf(out, "%*s timeout %ds\n", indent, "", cmd->timeoutSeconds); + return; + } + + /* + * CMD_ASSERT_ASSIGNED: grammar uses `=` not `is`. + * Distinguish wait-form (has timeout) from assert-form (no timeout). + */ + if (cmd->kind == CMD_ASSERT_ASSIGNED) + { + if (cmd->timeoutSeconds > 0) + { + fprintf(out, "%*swait until %s assigned-state = %s timeout %ds\n", + indent, "", cmd->service, cmd->state, cmd->timeoutSeconds); + } + else + { + fprintf(out, "%*sassert %s assigned-state = %s\n", + indent, "", cmd->service, cmd->state); + } + return; + } + + /* CMD_ASSERT_STATE with no timeout: instant check → canonical assert form */ + if (cmd->kind == CMD_ASSERT_STATE && cmd->timeoutSeconds == 0) + { + fprintf(out, "%*sassert %s state = %s\n", + indent, "", cmd->service, cmd->state); + return; + } + + /* CMD_ASSERT_STATE with timeout, and CMD_WAIT_STATE: polling wait */ + const char *op = "wait until"; + + if (cmd->passThroughCount > 0) + { + /* multi-line: target state, then "passing through" line */ + fprintf(out, "%*s%s %s state is %s\n", + indent, "", op, cmd->service, cmd->state); + fprintf(out, "%*s passing through", indent, ""); + for (int i = 0; i < cmd->passThroughCount; i++) + { + if (i > 0) + { + fprintf(out, ","); + } + fprintf(out, " %s", cmd->passThroughStates[i]); + } + fprintf(out, "\n"); + fprintf(out, "%*s timeout %ds\n", indent, "", cmd->timeoutSeconds); + return; + } + + /* simple single-line */ + if (cmd->timeoutSeconds > 0) + { + fprintf(out, "%*s%s %s state is %s timeout %ds\n", + indent, "", op, cmd->service, cmd->state, cmd->timeoutSeconds); + } + else + { + fprintf(out, "%*s%s %s state is %s\n", + indent, "", op, cmd->service, cmd->state); + } +} diff --git a/src/bin/pgaftest/cli_indent.h b/src/bin/pgaftest/cli_indent.h new file mode 100644 index 000000000..a625eead8 --- /dev/null +++ b/src/bin/pgaftest/cli_indent.h @@ -0,0 +1,15 @@ +/* + * src/bin/pgaftest/cli_indent.h + * pgaftest indent — parse a .pgaf file and rewrite it with canonical + * indentation. + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the PostgreSQL License. + */ + +#ifndef CLI_INDENT_H +#define CLI_INDENT_H + +void cli_indent(int argc, char **argv); + +#endif /* CLI_INDENT_H */ diff --git a/src/bin/pgaftest/cli_root.c b/src/bin/pgaftest/cli_root.c new file mode 100644 index 000000000..facb8837e --- /dev/null +++ b/src/bin/pgaftest/cli_root.c @@ -0,0 +1,575 @@ +/* + * src/bin/pgaftest/cli_root.c + * Top-level sub-command table for pgaftest. + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the PostgreSQL License. + */ + +#include +#include +#include +#include +#include + +#include "commandline.h" +#include "defaults.h" +#include "log.h" +#include "test_spec.h" +#include "test_runner.h" +#include "cli_demo.h" +#include "cli_indent.h" + +/* ----------------------------------------------------------------------- + * Shared option parsing + * ----------------------------------------------------------------------- */ + +typedef struct PgaftestOpts +{ + char specFile[1024]; + char workDir[1024]; /* empty = auto-derive from spec name */ + char stepName[64]; + char schedule[1024]; + char expected[1024]; + bool withTmux; + bool noCleanup; /* --no-cleanup: skip compose down after run */ + int verbose; +} PgaftestOpts; + +static PgaftestOpts pgaftestOpts; /* zero-initialised: workDir = "" */ + +/* + * derive_work_dir — build $TMPDIR/pgaftest/ + * + * Using TMPDIR (or /tmp when unset) + "pgaftest" + the test name means: + * - Every spec gets its own isolated directory. + * - Multiple instances of different specs can run concurrently. + * - The directory name matches the Docker Compose project name, so + * `docker compose -p basic_operation` addresses the right stack. + * + * Examples: + * tests/tap/specs/basic_operation.pgaf → /tmp/pgaftest/basic_operation + * failover.pgaf → /tmp/pgaftest/failover + */ +static void +derive_work_dir(const char *specPath, char *buf, int buflen) +{ + /* base name without directory */ + const char *base = strrchr(specPath, '/'); + base = base ? base + 1 : specPath; + + char name[256] = { 0 }; + strncpy(name, base, sizeof(name) - 1); + + /* strip .pgaf extension */ + char *dot = strrchr(name, '.'); + if (dot && strcmp(dot, ".pgaf") == 0) + { + *dot = '\0'; + } + + const char *tmpdir = getenv("TMPDIR"); + if (!tmpdir || *tmpdir == '\0') + { + tmpdir = "/tmp"; + } + + snprintf(buf, buflen, "%s/pgaftest/%s", tmpdir, name); +} + + +static struct option long_options[] = { + { "work-dir", required_argument, NULL, 'w' }, + { "schedule", required_argument, NULL, 'S' }, + { "expected", required_argument, NULL, 'E' }, + { "tmux", no_argument, NULL, 't' }, + { "no-cleanup", no_argument, NULL, 'n' }, + { "verbose", no_argument, NULL, 'v' }, + { "debug", no_argument, NULL, 'd' }, + { "help", no_argument, NULL, 'h' }, + { NULL, 0, NULL, 0 } +}; + +static int +pgaftest_getopts(int argc, char **argv) +{ + int c, option_index = 0; + + while ((c = getopt_long(argc, argv, "w:S:E:tnvdh", + long_options, &option_index)) != -1) + { + switch (c) + { + case 'w': + { + strncpy(pgaftestOpts.workDir, optarg, + sizeof(pgaftestOpts.workDir) - 1); + break; + } + + case 'S': + { + strncpy(pgaftestOpts.schedule, optarg, + sizeof(pgaftestOpts.schedule) - 1); + break; + } + + case 'E': + { + strncpy(pgaftestOpts.expected, optarg, + sizeof(pgaftestOpts.expected) - 1); + break; + } + + case 't': + { + pgaftestOpts.withTmux = true; + break; + } + + case 'n': + { + pgaftestOpts.noCleanup = true; + break; + } + + case 'v': + { + pgaftestOpts.verbose++; + log_set_level(LOG_DEBUG); + break; + } + + case 'd': + { + pgaftestOpts.verbose += 2; + log_set_level(LOG_TRACE); + break; + } + + case 'h': + { + commandline_help(stderr); + exit(EXIT_CODE_QUIT); + } + + default: + { + break; + } + } + } + return optind; +} + + +/* ----------------------------------------------------------------------- + * pgaftest run + * ----------------------------------------------------------------------- */ +static void +cli_run(int argc, char **argv) +{ + /* + * The subcommand library already called pgaftest_getopts() and stripped + * all options from argv before calling us. argv[0] is the spec file + * (if provided), argc is the number of remaining positional arguments. + */ + + /* Handle --schedule file */ + if (pgaftestOpts.schedule[0] != '\0') + { + FILE *f = fopen(pgaftestOpts.schedule, "r"); + if (!f) + { + log_error("Cannot open schedule \"%s\": %m", + pgaftestOpts.schedule); + exit(1); + } + + char line[256]; + int total = 0, failed = 0; + + while (fgets(line, sizeof(line), f)) + { + /* strip newline and comments */ + char *nl = strchr(line, '\n'); + if (nl) + { + *nl = '\0'; + } + char *hash = strchr(line, '#'); + if (hash) + { + *hash = '\0'; + } + + /* skip blank lines */ + char *p = line; + while (*p == ' ' || *p == '\t') + { + p++; + } + if (*p == '\0') + { + continue; + } + + /* build spec path from schedule entry */ + char specPath[1024]; + if (strchr(p, '/')) + { + strncpy(specPath, p, sizeof(specPath) - 1); + } + else + { + snprintf(specPath, sizeof(specPath), + "tests/tap/specs/%s.pgaf", p); + } + + char workDir[1024]; + if (pgaftestOpts.workDir[0] != '\0') + { + /* explicit --work-dir: append spec name as subdir */ + const char *base = strrchr(specPath, '/'); + base = base ? base + 1 : specPath; + char name[128]; + strncpy(name, base, sizeof(name) - 1); + char *dot = strrchr(name, '.'); + if (dot) + { + *dot = '\0'; + } + snprintf(workDir, sizeof(workDir), + "%s/%s", pgaftestOpts.workDir, name); + } + else + { + derive_work_dir(specPath, workDir, sizeof(workDir)); + } + + TestSpec *spec = parse_test_spec(specPath); + if (!spec) + { + failed++; + total++; + continue; + } + + total++; + if (!runner_run(spec, workDir, pgaftestOpts.noCleanup)) + { + failed++; + } + } + fclose(f); + + fprintf(stderr, "\nSchedule complete: %d/%d passed\n", + total - failed, total); + exit(failed > 0 ? 1 : 0); + } + + /* Single spec file */ + if (argc < 1 || argv[0] == NULL) + { + log_error("Usage: pgaftest run [--schedule ] []"); + exit(1); + } + + strncpy(pgaftestOpts.specFile, argv[0], + sizeof(pgaftestOpts.specFile) - 1); + + if (pgaftestOpts.workDir[0] == '\0') + { + derive_work_dir(pgaftestOpts.specFile, + pgaftestOpts.workDir, sizeof(pgaftestOpts.workDir)); + } + + TestSpec *spec = parse_test_spec(pgaftestOpts.specFile); + if (!spec) + { + exit(1); + } + + bool ok = runner_run(spec, pgaftestOpts.workDir, pgaftestOpts.noCleanup); + exit(ok ? 0 : 1); +} + + +/* ----------------------------------------------------------------------- + * pgaftest setup + * ----------------------------------------------------------------------- */ +static void +cli_setup(int argc, char **argv) +{ + if (argc < 1 || argv[0] == NULL) + { + log_error("Usage: pgaftest setup [--tmux] [--work-dir ]"); + exit(1); + } + + strncpy(pgaftestOpts.specFile, argv[0], + sizeof(pgaftestOpts.specFile) - 1); + + if (pgaftestOpts.workDir[0] == '\0') + { + derive_work_dir(pgaftestOpts.specFile, + pgaftestOpts.workDir, sizeof(pgaftestOpts.workDir)); + } + + TestSpec *spec = parse_test_spec(pgaftestOpts.specFile); + if (!spec) + { + exit(1); + } + + bool ok = runner_setup(spec, pgaftestOpts.workDir, + pgaftestOpts.withTmux); + exit(ok ? 0 : 1); +} + + +/* ----------------------------------------------------------------------- + * pgaftest step + * ----------------------------------------------------------------------- */ +static void +cli_step(int argc, char **argv) +{ + if (argc < 1 || argv[0] == NULL) + { + log_error("Usage: pgaftest step [--work-dir ]"); + exit(1); + } + + /* step name is positional */ + strncpy(pgaftestOpts.stepName, argv[0], + sizeof(pgaftestOpts.stepName) - 1); + + /* We need the spec file too — look for it in workDir */ + char specPath[1024]; + snprintf(specPath, sizeof(specPath), "%s/spec.pgaf", + pgaftestOpts.workDir); + + /* If there's a second positional arg, treat it as the spec path */ + if (argc >= 2 && argv[1] != NULL) + { + strncpy(specPath, argv[1], sizeof(specPath) - 1); + } + + /* derive work dir from spec path when not given explicitly */ + if (pgaftestOpts.workDir[0] == '\0') + { + derive_work_dir(specPath, pgaftestOpts.workDir, + sizeof(pgaftestOpts.workDir)); + } + + TestSpec *spec = parse_test_spec(specPath); + if (!spec) + { + exit(1); + } + + bool ok = runner_step(spec, pgaftestOpts.workDir, + pgaftestOpts.stepName); + exit(ok ? 0 : 1); +} + + +/* ----------------------------------------------------------------------- + * pgaftest prepare [] + * + * Writes docker-compose.yml, *.ini files, and a Makefile to the output + * directory. Prints the docker compose command to stdout. + * ----------------------------------------------------------------------- */ +static void +cli_prepare(int argc, char **argv) +{ + if (argc < 1 || argv[0] == NULL) + { + log_error("Usage: pgaftest prepare []"); + exit(1); + } + + strncpy(pgaftestOpts.specFile, argv[0], sizeof(pgaftestOpts.specFile) - 1); + + const char *outDir = (argc >= 2 && argv[1] && argv[1][0] != '-') + ? argv[1] : NULL; + + TestSpec *spec = parse_test_spec(pgaftestOpts.specFile); + if (!spec) + { + exit(1); + } + + bool ok = runner_prepare(spec, outDir); + exit(ok ? 0 : 1); +} + + +/* ----------------------------------------------------------------------- + * pgaftest show + * ----------------------------------------------------------------------- */ +static void +cli_show(int argc, char **argv) +{ + if (argc < 1 || argv[0] == NULL) + { + log_error("Usage: pgaftest show "); + exit(1); + } + + strncpy(pgaftestOpts.specFile, argv[0], + sizeof(pgaftestOpts.specFile) - 1); + + TestSpec *spec = parse_test_spec(pgaftestOpts.specFile); + if (!spec) + { + exit(1); + } + + bool ok = runner_show(spec); + exit(ok ? 0 : 1); +} + + +/* ----------------------------------------------------------------------- + * pgaftest down + * ----------------------------------------------------------------------- */ +static void +cli_down(int argc, char **argv) +{ + /* optional positional: spec file to derive work dir from */ + if (argc >= 1 && argv[0] && argv[0][0] != '-') + { + strncpy(pgaftestOpts.specFile, argv[0], + sizeof(pgaftestOpts.specFile) - 1); + } + + if (pgaftestOpts.workDir[0] == '\0' && pgaftestOpts.specFile[0] != '\0') + { + derive_work_dir(pgaftestOpts.specFile, + pgaftestOpts.workDir, sizeof(pgaftestOpts.workDir)); + } + + /* spec file is optional for `down` — teardown may not need it */ + char specPath[1024]; + if (pgaftestOpts.specFile[0] != '\0') + { + strncpy(specPath, pgaftestOpts.specFile, sizeof(specPath) - 1); + } + else + { + snprintf(specPath, sizeof(specPath), "%s/spec.pgaf", + pgaftestOpts.workDir); + } + + TestSpec *spec = NULL; + if (access(specPath, R_OK) == 0) + { + spec = parse_test_spec(specPath); + } + + /* If no spec, just run compose down */ + if (!spec) + { + int rc = system("docker compose down --volumes --remove-orphans 2>&1"); + exit(rc == 0 ? 0 : 1); + } + + bool ok = runner_down(spec, pgaftestOpts.workDir); + exit(ok ? 0 : 1); +} + + +/* ----------------------------------------------------------------------- + * Root command table + * ----------------------------------------------------------------------- */ + +static CommandLine run_command = + make_command("run", + "Run a .pgaf spec headlessly (CI mode, TAP output)", + "[options] []", + " --schedule Run all specs listed in schedule file\n" + " --expected Directory of expected output files\n" + " --work-dir Working directory\n" + " (default: $TMPDIR/pgaftest/)\n" + " --no-cleanup Leave the compose stack running after the\n" + " run (pass or fail) for post-mortem inspection.\n" + " Use `pgaftest down ` to clean up.\n" + " --verbose Enable DEBUG log level\n" + " --debug Enable TRACE log level\n", + pgaftest_getopts, cli_run); + +static CommandLine setup_command = + make_command("setup", + "Bring up a cluster from a spec file (interactive mode)", + "[options] ", + " --tmux Launch a tmux session after setup\n" + " --work-dir Working directory\n" + " (default: $TMPDIR/pgaftest/)\n" + " --verbose Enable DEBUG log level\n" + " --debug Enable TRACE log level\n", + pgaftest_getopts, cli_setup); + +static CommandLine step_command = + make_command("step", + "Run a single named step against a live compose stack", + "[options] []", + " --work-dir Working directory (default: /tmp/pgaftest)\n", + pgaftest_getopts, cli_step); + +static CommandLine show_command = + make_command("show", + "Print the docker-compose.yml that would be generated", + "[options] ", + "", + pgaftest_getopts, cli_show); + +static CommandLine prepare_command = + make_command("prepare", + "Write compose files and Makefile to a directory for manual use", + " []", + " Path to the spec file\n" + " Output directory (default: -compose/)\n", + pgaftest_getopts, cli_prepare); + +static CommandLine down_command = + make_command("down", + "Run teardown block and stop the compose stack", + "[options]", + " --work-dir Working directory (default: /tmp/pgaftest)\n", + pgaftest_getopts, cli_down); + +extern void cli_indent(int argc, char **argv); + +static CommandLine indent_command = + make_command("indent", + "Parse a .pgaf spec and rewrite it with canonical indentation", + "", + "", + pgaftest_getopts, cli_indent); + +static CommandLine *root_subcommands[] = { + &run_command, + &setup_command, + &step_command, + &show_command, + &prepare_command, + &down_command, + &indent_command, + &pgaftest_demo_command, + NULL +}; + +CommandLine pgaftest_root = + make_command_set("pgaftest", + "pg_auto_failover test runner", + "[command] [options]", + " run Run a .pgaf spec (CI mode)\n" + " setup Bring up a cluster interactively\n" + " step Run one named step\n" + " show Print generated docker-compose.yml\n" + " prepare Write compose files + Makefile to a directory\n" + " down Tear down the cluster\n" + " indent Rewrite a spec with canonical indentation\n" + " demo Demo application\n", + NULL, root_subcommands); diff --git a/src/bin/pgaftest/compose_gen.c b/src/bin/pgaftest/compose_gen.c new file mode 100644 index 000000000..f55e9f488 --- /dev/null +++ b/src/bin/pgaftest/compose_gen.c @@ -0,0 +1,1252 @@ +/* + * src/bin/pgaftest/compose_gen.c + * Generate a docker-compose.yml and per-node pg_autoctl_node.ini files + * from a TestCluster spec. + * + * Design principle — uniform containers + * ====================================== + * Every container in the generated compose file uses: + * - the same Docker image (PGAF_IMAGE env var or a local build) + * - the same command: pg_autoctl node run /etc/pgaf/node.ini + * - the same config file location inside the container + * + * Node-specific behaviour (monitor vs postgres vs coordinator vs worker, + * candidate-priority, replication-quorum, formation, group) is encoded in + * the per-node pg_autoctl_node.ini file, which is bind-mounted into the + * container at /etc/pgaf/node.ini. + * + * This means: + * - No bespoke container command per node type. + * - The running supervisor watches /etc/pgaf/node.ini via inotify/mtime + * and converges mutable settings when the file is edited externally. + * - Changing candidate-priority or replication-quorum for a node is as + * simple as editing the ini file on the host — the container picks it + * up without a restart. + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the PostgreSQL License. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "compose_gen.h" +#include "pgsetup.h" +#include "log.h" +#include "file_utils.h" /* sformat */ +#include "string_utils.h" + +/* + * Monitor URI used by data nodes to register. + * The monitor hostname is always "monitor" inside the compose network. + */ +#define MONITOR_PGURI \ + "postgresql://autoctl_node@monitor/pg_auto_failover" + +/* Fixed path inside every container */ +#define NODE_INI_PATH "/etc/pgaf/node.ini" + +/* Fixed PGDATA location inside every container */ +#define NODE_PGDATA "/var/lib/postgres/pgaf" + +/* Debian-style PGDATA prefix: /var/lib/postgresql// */ +#define DEBIAN_PGDATA_PREFIX "/var/lib/postgresql" + +static const char * +debian_pg_version(void) +{ + const char *v = getenv("PGVERSION"); + return (v && v[0]) ? v : "17"; +} + + +/* SSL cert directory inside every container when ssl needs CA-signed certs */ +#define SSL_DIR_IN_CONTAINER "/etc/pgaf/ssl" + +/* + * Shell snippet prepended to the container start command when CA-signed certs + * are in use. + * + * - Server cert/key copied to /var/lib/postgres/ (volume root, writable) so + * PostgreSQL can open the key file as the container user. + * - Client cert/key copied to ~/.postgresql/ so libpq uses them automatically + * for all outbound connections (cert auth to monitor and peers). + * - CA cert copied to ~/.postgresql/root.crt so libpq trusts server certs. + */ +#define SSL_COPY_CERTS_CMD \ + "cp " SSL_DIR_IN_CONTAINER "/server/server.crt /var/lib/postgres/server.crt" \ + " && cp " SSL_DIR_IN_CONTAINER \ + "/server/server.key /var/lib/postgres/server.key" \ + " && chmod 0600 /var/lib/postgres/server.key" \ + " && mkdir -p /var/lib/postgres/.postgresql" \ + " && cp " \ + SSL_DIR_IN_CONTAINER \ + "/client/postgresql.crt /var/lib/postgres/.postgresql/postgresql.crt" \ + " && cp " \ + SSL_DIR_IN_CONTAINER \ + "/client/postgresql.key /var/lib/postgres/.postgresql/postgresql.key" \ + " && chmod 0600 /var/lib/postgres/.postgresql/postgresql.key" \ + " && cp " \ + SSL_DIR_IN_CONTAINER "/ca.crt /var/lib/postgres/.postgresql/root.crt" \ + " &&" + + +/* + * ssl_needs_certs returns true when the ssl mode requires CA-signed server + * certificates to be generated on the host: verify-ca and verify-full. + */ +static bool +ssl_needs_certs(const char *ssl) +{ + return strcmp(ssl, "verify-ca") == 0 || strcmp(ssl, "verify-full") == 0; +} + + +/* + * run_openssl runs a single openssl command (argv-style) and returns true on + * success. The last element of argv[] must be NULL. + */ +static bool +run_openssl(const char *const *argv) +{ + char cmd[4096] = "openssl"; + int pos = strlen(cmd); + + for (int i = 1; argv[i]; i++) + { + pos += snprintf(cmd + pos, sizeof(cmd) - pos, " %s", argv[i]); + if (pos >= (int) sizeof(cmd) - 1) + { + log_error("openssl command too long"); + return false; + } + } + + log_debug("Running: %s", cmd); + int rc = system(cmd); + if (rc != 0) + { + log_error("Command failed (exit %d): %s", rc, cmd); + return false; + } + return true; +} + + +/* + * compose_gen_write_ssl_certs generates the CA and per-service server + * certificates in /ssl/ using openssl. Called from + * runner_compose_generate() before compose_gen_write() so the volume paths + * already exist when the compose file is written. + * + * Layout on the host: + * ssl/ca.crt — shared CA certificate + * ssl/ca.key — CA private key (stays on host) + * ssl//server.crt — server certificate for service + * ssl//server.key — server private key for service + * + * The CA cert and the per-service dir are bind-mounted into each container so + * pg_autoctl can find them at the paths recorded in the ini [ssl] section. + */ +bool +compose_gen_write_ssl_certs(const TestCluster *cluster, const char *workDir) +{ + if (!ssl_needs_certs(cluster->ssl)) + { + return true; + } + + char sslDir[MAXPGPATH]; + sformat(sslDir, sizeof(sslDir), "%s/ssl", workDir); + + /* Create ssl/ directory */ + if (mkdir(sslDir, 0755) != 0 && errno != EEXIST) + { + log_error("Failed to create %s: %m", sslDir); + return false; + } + + char caKey[MAXPGPATH], caCrt[MAXPGPATH]; + sformat(caKey, sizeof(caKey), "%s/ca.key", sslDir); + sformat(caCrt, sizeof(caCrt), "%s/ca.crt", sslDir); + + /* Generate CA key + self-signed cert with basicConstraints=CA:TRUE */ + bool caRegenerated = false; + if (access(caCrt, F_OK) != 0) + { + const char *genCA[] = { + "openssl", "req", "-new", "-x509", "-nodes", "-text", + "-days", "3650", + "-keyout", caKey, + "-out", caCrt, + "-subj", "/CN=root.pgautofailover.ca", + "-addext", "basicConstraints=critical,CA:TRUE", + "-addext", "keyUsage=critical,keyCertSign,cRLSign", + NULL + }; + if (!run_openssl(genCA)) + { + return false; + } + + chmod(caKey, 0600); /* CA key stays private; not mounted into containers */ + log_info("Generated CA certificate at %s", caCrt); + caRegenerated = true; + } + + /* + * Generate server cert for each service: monitor + all nodes. + * We build a flat list of service names to iterate. + */ + const char *services[PGAF_MAX_NODES + 1]; /* +1 for monitor */ + int svcCount = 0; + + if (cluster->withMonitor) + { + services[svcCount++] = "monitor"; + } + + if (cluster->secondMonitorName[0]) + { + services[svcCount++] = cluster->secondMonitorName; + } + + for (int fi = 0; fi < cluster->formationCount; fi++) + { + const TestFormation *form = &cluster->formations[fi]; + for (int ni = 0; ni < form->nodeCount; ni++) + { + services[svcCount++] = form->nodes[ni].name; + } + } + + char serialFile[MAXPGPATH]; + sformat(serialFile, sizeof(serialFile), "%s/ca.srl", sslDir); + + for (int i = 0; i < svcCount; i++) + { + const char *svc = services[i]; + char svcDir[MAXPGPATH]; + sformat(svcDir, sizeof(svcDir), "%s/%s", sslDir, svc); + + if (mkdir(svcDir, 0755) != 0 && errno != EEXIST) + { + log_error("Failed to create %s: %m", svcDir); + return false; + } + + char srvKey[MAXPGPATH], srvCrt[MAXPGPATH], srvCsr[MAXPGPATH]; + sformat(srvKey, sizeof(srvKey), "%s/server.key", svcDir); + sformat(srvCrt, sizeof(srvCrt), "%s/server.crt", svcDir); + sformat(srvCsr, sizeof(srvCsr), "%s/server.csr", svcDir); + + if (access(srvCrt, F_OK) == 0 && !caRegenerated) + { + continue; /* already generated and CA hasn't changed */ + } + char cn[128]; + sformat(cn, sizeof(cn), "/CN=%s.pgautofailover.ca", svc); + + const char *genReq[] = { + "openssl", "req", "-new", "-nodes", "-text", + "-out", srvCsr, "-keyout", srvKey, + "-subj", cn, + NULL + }; + if (!run_openssl(genReq)) + { + return false; + } + + /* 0644: container user must read this for the cp-and-chmod step */ + chmod(srvKey, 0644); + + const char *signSrv[] = { + "openssl", "x509", "-req", "-in", srvCsr, + "-text", "-days", "365", + "-CA", caCrt, "-CAkey", caKey, "-CAcreateserial", + "-out", srvCrt, + NULL + }; + if (!run_openssl(signSrv)) + { + return false; + } + + log_info("Generated server certificate for %s at %s", svc, srvCrt); + } + + /* + * Generate client certificate with CN=autoctl_node, used by pg_autoctl + * processes to authenticate to PostgreSQL when ssl.auth = cert. + * Placed in ssl/client/ and mounted read-only into each container; + * the container start command copies it to ~/.postgresql/ with correct + * ownership so libpq can use it for all outbound connections. + */ + char clientDir[MAXPGPATH], clientCrt[MAXPGPATH], + clientKey[MAXPGPATH], clientCsr[MAXPGPATH]; + sformat(clientDir, sizeof(clientDir), "%s/client", sslDir); + sformat(clientCrt, sizeof(clientCrt), "%s/postgresql.crt", clientDir); + sformat(clientKey, sizeof(clientKey), "%s/postgresql.key", clientDir); + sformat(clientCsr, sizeof(clientCsr), "%s/postgresql.csr", clientDir); + + if (mkdir(clientDir, 0755) != 0 && errno != EEXIST) + { + log_error("Failed to create %s: %m", clientDir); + return false; + } + + if (access(clientCrt, F_OK) != 0 || caRegenerated) + { + const char *genReq[] = { + "openssl", "req", "-new", "-nodes", "-text", + "-out", clientCsr, "-keyout", clientKey, + "-subj", "/CN=autoctl_node", + NULL + }; + if (!run_openssl(genReq)) + { + return false; + } + + /* 0644: container user must read this for the cp-and-chmod step */ + chmod(clientKey, 0644); + + const char *signClient[] = { + "openssl", "x509", "-req", "-in", clientCsr, + "-text", "-days", "365", + "-CA", caCrt, "-CAkey", caKey, "-CAcreateserial", + "-out", clientCrt, + NULL + }; + if (!run_openssl(signClient)) + { + return false; + } + + log_info("Generated client certificate at %s", clientCrt); + } + + return true; +} + + +/* + * image_stanza writes either `image:` (when PGAF_IMAGE is set or the cluster + * spec provides an image) or a `build:` stanza. + */ +static void +write_image_stanza_target(FILE *f, const TestCluster *cluster, + const char *contextDir, const char *target) +{ + /* + * The cluster image (from `image "..."` in the spec) is for data services + * only. The pgaftest service always builds from --target pgaftest so that + * the test runner binary is present regardless of which cluster image is + * used. + */ + bool isTestRunner = (strcmp(target, "pgaftest") == 0); + + /* cluster-level image overrides PGAF_IMAGE env var */ + const char *img = cluster->image[0] ? cluster->image : getenv("PGAF_IMAGE"); + + if (img && *img && !isTestRunner) + { + fprintf(f, " image: \"%s\"\n", img); + } + else + { + fprintf(f, + " build:\n" + " context: \"%s\"\n" + " target: %s\n", + contextDir, target); + } +} + + +static void +write_image_stanza(FILE *f, const TestCluster *cluster, const char *contextDir) +{ + write_image_stanza_target(f, cluster, contextDir, "run"); +} + + +/* ----------------------------------------------------------------------- + * compose_gen_write — the docker-compose.yml + * ----------------------------------------------------------------------- */ + +/* + * compose_gen_write generates the docker-compose.yml. + * + * Every service uses: + * command: pg_autoctl node run /etc/pgaf/node.ini + * + * The per-node ini file is bind-mounted from the workdir. + */ + +/* + * Pick a random free TCP port in the range [32768, 60999]. + * Tries up to 64 candidates; exits with a log_fatal if none found. + * + * Ports already handed out in this process are remembered in a static array + * so that consecutive calls never return the same port even though the OS + * releases the probe socket between calls. + */ +static int +pick_free_port(void) +{ + static int reserved[64]; + static int reserved_count = 0; + + srand((unsigned) time(NULL) ^ (unsigned) getpid()); + for (int attempt = 0; attempt < 64; attempt++) + { + int port = 32768 + rand() % (60999 - 32768 + 1); + + /* skip ports already given out this process */ + bool already_used = false; + for (int i = 0; i < reserved_count; i++) + { + if (reserved[i] == port) + { + already_used = true; + break; + } + } + if (already_used) + { + continue; + } + + int fd = socket(AF_INET, SOCK_STREAM, 0); + if (fd < 0) + { + continue; + } + + int opt = 1; + setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); + + struct sockaddr_in addr = { + .sin_family = AF_INET, + .sin_port = htons((uint16_t) port), + .sin_addr = { .s_addr = htonl(INADDR_LOOPBACK) }, + }; + int rc = bind(fd, (struct sockaddr *) &addr, sizeof(addr)); + close(fd); + + if (rc == 0) + { + if (reserved_count < 64) + { + reserved[reserved_count++] = port; + } + return port; + } + } + log_fatal("Could not find a free TCP port for monitor exposure"); + exit(1); +} + + +bool +compose_gen_write(TestCluster *cluster, + const char *path, + const char *projectName, + const char *contextDir, + const char *specFile) +{ + FILE *f = fopen(path, "w"); + if (!f) + { + log_error("Failed to open \"%s\" for writing: %m", path); + return false; + } + + fprintf(f, "# Generated by pgaftest — do not edit manually\n"); + fprintf(f, "# Project: %s\n\n", projectName); + fprintf(f, "services:\n"); + + /* ---- monitor (optional) ---- */ + if (cluster->withMonitor) + { + /* allocate a random free host port for the monitor's postgres */ + if (cluster->monitorHostPort == 0) + { + cluster->monitorHostPort = pick_free_port(); + } + + fprintf(f, " monitor:\n"); + if (cluster->monitorDebianCluster[0]) + { + write_image_stanza_target(f, cluster, contextDir, "debian"); + } + else if (cluster->monitorImageTarget[0]) + { + write_image_stanza_target(f, cluster, contextDir, + cluster->monitorImageTarget); + } + else + { + write_image_stanza(f, cluster, contextDir); + } + + char monitor_pgdata[MAXPGPATH]; + if (cluster->monitorDebianCluster[0]) + { + snprintf(monitor_pgdata, sizeof(monitor_pgdata), + DEBIAN_PGDATA_PREFIX "/%s/%s", + debian_pg_version(), cluster->monitorDebianCluster); + } + else + { + strlcpy(monitor_pgdata, NODE_PGDATA, sizeof(monitor_pgdata)); + } + + fprintf(f, + " hostname: monitor\n" + " volumes:\n" + " - monitor_data:/var/lib/postgres:rw\n" + " - ./monitor.ini:" NODE_INI_PATH ":ro\n"); + if (ssl_needs_certs(cluster->ssl)) + { + fprintf(f, + " - ./ssl/ca.crt:" SSL_DIR_IN_CONTAINER "/ca.crt:ro\n" + " - ./ssl/monitor:" + SSL_DIR_IN_CONTAINER "/server:ro\n" + " - ./ssl/client:" + SSL_DIR_IN_CONTAINER "/client:ro\n"); + } + if (cluster->bindSource) + { + fprintf(f, + " - %s:/usr/src/pg_auto_failover:rw\n", contextDir); + } + fprintf(f, + " environment:\n" + " PGDATA: %s\n", + monitor_pgdata); + if (cluster->extensionVersion[0]) + { + fprintf(f, + " PG_AUTOCTL_EXTENSION_VERSION: \"%s\"\n", + cluster->extensionVersion); + } + fprintf(f, + " ports:\n" + " - \"%d:5432\"\n", + cluster->monitorHostPort); + + fprintf(f, + " command: [\"/bin/sh\", \"-c\"," + " \"%s rm -f /tmp/pg_autoctl%s/pg_autoctl.pid" + " && exec pg_autoctl node run " NODE_INI_PATH "\"]\n" + " stop_grace_period: 60s\n\n", + ssl_needs_certs(cluster->ssl) ? SSL_COPY_CERTS_CMD : "", + monitor_pgdata); + } + + /* ---- second monitor (initially stopped, for replace-monitor tests) ---- */ + if (cluster->secondMonitorName[0]) + { + if (cluster->secondMonitorHostPort == 0) + { + cluster->secondMonitorHostPort = pick_free_port(); + } + + const char *svc = cluster->secondMonitorName; + fprintf(f, " %s:\n", svc); + write_image_stanza(f, cluster, contextDir); + fprintf(f, + " hostname: %s\n" + " volumes:\n" + " - %s_data:/var/lib/postgres:rw\n" + " - ./%s.ini:" NODE_INI_PATH ":ro\n", + svc, svc, svc); + if (ssl_needs_certs(cluster->ssl)) + { + fprintf(f, + " - ./ssl/ca.crt:" SSL_DIR_IN_CONTAINER "/ca.crt:ro\n" + " - ./ssl/%s:" + SSL_DIR_IN_CONTAINER "/server:ro\n" + " - ./ssl/client:" + SSL_DIR_IN_CONTAINER "/client:ro\n", + svc); + } + if (cluster->bindSource) + { + fprintf(f, + " - %s:/usr/src/pg_auto_failover:rw\n", contextDir); + } + fprintf(f, + " environment:\n" + " PGDATA: " NODE_PGDATA "\n"); + fprintf(f, + " ports:\n" + " - \"%d:5432\"\n", + cluster->secondMonitorHostPort); + fprintf(f, + " command: [\"/bin/sh\", \"-c\"," + " \"%s rm -f /tmp/pg_autoctl" NODE_PGDATA "/pg_autoctl.pid" + " && exec pg_autoctl node run " + NODE_INI_PATH "\"]\n" + " stop_grace_period: 60s\n\n", + ssl_needs_certs(cluster->ssl) ? SSL_COPY_CERTS_CMD : ""); + } + + /* ---- data nodes — iterate all formations ---- */ + + /* + * Non-default formations are listed in [formation ] sections of the + * monitor.ini. pg_autoctl node run reads them and creates each one after + * the monitor is initialized, before starting the supervisor. + * Data nodes retry registration until their formation exists. + */ + const TestNode *firstNode = NULL; + + for (int fi = 0; fi < cluster->formationCount; fi++) + { + const TestFormation *form = &cluster->formations[fi]; + + for (int ni = 0; ni < form->nodeCount; ni++) + { + const TestNode *n = &form->nodes[ni]; + fprintf(f, " %s:\n", n->name); + if (n->debianCluster[0]) + { + write_image_stanza_target(f, cluster, contextDir, "debian"); + } + else + { + write_image_stanza(f, cluster, contextDir); + } + + /* debian PGDATA: /var/lib/postgresql// */ + char node_pgdata[MAXPGPATH]; + if (n->debianCluster[0]) + { + snprintf(node_pgdata, sizeof(node_pgdata), + DEBIAN_PGDATA_PREFIX "/%s/%s", + debian_pg_version(), n->debianCluster); + } + else + { + strlcpy(node_pgdata, NODE_PGDATA, sizeof(node_pgdata)); + } + + fprintf(f, + " hostname: %s\n" + " volumes:\n" + " - %s_data:/var/lib/postgres:rw\n" + " - ./%s.ini:" NODE_INI_PATH ":%s\n", + n->name, + n->name, + n->name, + n->launchDeferred ? "rw" : "ro"); + if (ssl_needs_certs(cluster->ssl)) + { + fprintf(f, + " - ./ssl/ca.crt:" SSL_DIR_IN_CONTAINER "/ca.crt:ro\n" + " - ./ssl/%s:" + SSL_DIR_IN_CONTAINER "/server:ro\n" + " - ./ssl/client:" + SSL_DIR_IN_CONTAINER "/client:ro\n", + n->name); + } + for (int vi = 0; vi < n->volumeCount; vi++) + { + fprintf(f, " - %s_%s:%s:rw\n", + n->volumes[vi].name, n->name, n->volumes[vi].path); + } + fprintf(f, + " environment:\n" + " PGDATA: %s\n" + " PGUSER: demo\n" + " PGDATABASE: demo\n" + " PG_AUTOCTL_TEST_DELAY: \"1\"\n" + " expose:\n" + " - 5432\n", + node_pgdata); + + /* + * No depends_on: keeper retry loops handle monitor not yet ready, + * and the formation wait in keeper_register_and_init handles the + * case where the formation hasn't been created yet. + */ + + /* per-node ssl override may differ from cluster default */ + const char *node_ssl = n->ssl[0] ? n->ssl : cluster->ssl; + fprintf(f, + " command: [\"/bin/sh\", \"-c\"," + " \"%s rm -f /tmp/pg_autoctl%s/pg_autoctl.pid" + " && exec pg_autoctl node run " NODE_INI_PATH "\"]\n" + " stop_grace_period: 60s\n", + ssl_needs_certs(node_ssl) ? SSL_COPY_CERTS_CMD : "", + node_pgdata); + + /* + * With a monitor: the first data node gets a healthcheck so that + * subsequent nodes use service_healthy depends_on. This ensures + * node1 has registered with the monitor (and become the initial + * primary) before any other node starts, making the initial + * election deterministic. + * + * Without a monitor (no-monitor nodes): the FSM is driven + * manually via pg_autoctl manual fsm assign, so postgres is not + * running when the container starts. No healthcheck; subsequent + * nodes use service_started so they launch as soon as node1 has + * started (they don't need postgres to be ready yet). + */ + if (!firstNode && cluster->withMonitor) + { + fprintf(f, + " healthcheck:\n" + " test: [\"CMD\", \"pg_autoctl\", \"status\"," + " \"--pgdata\", \"%s\"]\n" + " interval: 2s\n" + " timeout: 5s\n" + " retries: 30\n" + " start_period: 15s\n", + node_pgdata); + } + + if (firstNode) + { + fprintf(f, + " depends_on:\n" + " %s:\n" + " condition: %s\n", + firstNode->name, + cluster->withMonitor ? "service_healthy" : "service_started"); + } + fprintf(f, "\n"); + + if (!firstNode) + { + firstNode = n; + } + } + } + + /* + * ---- pgaftest service ---- + * + * When specFile is provided, include a pgaftest service that runs the + * spec from inside the compose network. Benefits: + * - direct TCP to monitor/nodes — LISTEN/NOTIFY works, no pg_hba issue + * - pg_autoctl commands run locally (just need --monitor ) + * - compose stop/start and network disconnect/connect via docker socket + * + * CI usage: docker compose up --exit-code-from pgaftest + */ + if (specFile) + { + /* Determine which service pgaftest must wait for before starting. */ + /* It needs the cluster fully ready: monitor healthy + all nodes started. */ + fprintf(f, " pgaftest:\n"); + write_image_stanza_target(f, cluster, contextDir, "pgaftest"); + + /* Build monitor pguri for PG_AUTOCTL_MONITOR */ + char monitorPguri[512]; + if (cluster->monitorPassword[0]) + { + sformat(monitorPguri, sizeof(monitorPguri), + "postgres://autoctl_node:%s@monitor/pg_auto_failover", + cluster->monitorPassword); + } + else + { + sformat(monitorPguri, sizeof(monitorPguri), + "postgres://autoctl_node@monitor/pg_auto_failover"); + } + + /* + * pgaftest has its own ping loop that waits for the monitor before + * starting, so no depends_on is needed. + */ + + /* Derive work dir (directory containing the compose file). */ + char workDir[MAXPGPATH]; + strlcpy(workDir, path, sizeof(workDir)); + char *slash = strrchr(workDir, '/'); + if (slash) + { + *slash = '\0'; + } + else + { + strlcpy(workDir, ".", sizeof(workDir)); + } + + fprintf(f, + " user: root\n" + " volumes:\n" + " - %s:/spec.pgaf:ro\n" + " - /var/run/docker.sock:/var/run/docker.sock\n" + " - %s:%s:ro\n", + specFile, workDir, workDir); + if (ssl_needs_certs(cluster->ssl)) + { + fprintf(f, + " - %s/ssl/ca.crt:/root/.postgresql/root.crt:ro\n" + " - %s/ssl/client/postgresql.crt:/root/.postgresql/postgresql.crt:ro\n" + " - %s/ssl/client/postgresql.key:/root/.postgresql/postgresql.key:ro\n", + workDir, workDir, workDir); + } + fprintf(f, + " environment:\n" + " PGAFTEST_COMPOSE_SERVICE: \"1\"\n" + " PGAFTEST_HOST_WORK_DIR: \"%s\"\n" + " COMPOSE_PROJECT_NAME: \"%s\"\n", + workDir, projectName); + if (cluster->withMonitor) + { + fprintf(f, " PG_AUTOCTL_MONITOR: \"%s\"\n", monitorPguri); + } + fprintf(f, " command: [\"pgaftest\", \"run\", \"/spec.pgaf\"]\n\n"); + } + + /* ---- volumes ---- */ + fprintf(f, "volumes:\n"); + if (cluster->withMonitor) + { + fprintf(f, " monitor_data:\n"); + } + if (cluster->secondMonitorName[0]) + { + fprintf(f, " %s_data:\n", cluster->secondMonitorName); + } + for (int fi = 0; fi < cluster->formationCount; fi++) + { + const TestFormation *form = &cluster->formations[fi]; + for (int ni = 0; ni < form->nodeCount; ni++) + { + const TestNode *n = &form->nodes[ni]; + fprintf(f, " %s_data:\n", n->name); + for (int vi = 0; vi < n->volumeCount; vi++) + { + fprintf(f, " %s_%s:\n", n->volumes[vi].name, n->name); + } + } + } + + fclose(f); + log_info("Wrote docker-compose.yml to \"%s\"", path); + return true; +} + + +/* ----------------------------------------------------------------------- + * compose_gen_write_monitor_ini + * ----------------------------------------------------------------------- */ +bool +compose_gen_write_monitor_ini(const TestCluster *cluster, const char *dir) +{ + if (!cluster->withMonitor) + { + return true; + } + + char path[1024]; + sformat(path, sizeof(path), "%s/monitor.ini", dir); + + FILE *f = fopen(path, "w"); + if (!f) + { + log_error("Failed to open \"%s\" for writing: %m", path); + return false; + } + + char mon_pgdata[MAXPGPATH]; + if (cluster->monitorDebianCluster[0]) + { + snprintf(mon_pgdata, sizeof(mon_pgdata), + DEBIAN_PGDATA_PREFIX "/%s/%s", + debian_pg_version(), cluster->monitorDebianCluster); + } + else + { + strlcpy(mon_pgdata, NODE_PGDATA, sizeof(mon_pgdata)); + } + + fprintf(f, + "# Generated by pgaftest — do not edit manually\n" + "# Monitor node configuration\n" + "\n" + "[node]\n" + "kind = monitor\n" + "hostname = monitor\n" + "port = 5432\n" + "\n" + "[postgresql]\n" + "pgdata = %s\n" + "\n" + "[options]\n" + "ssl = %s\n" + "auth = %s\n" + "pg_hba_lan = false\n", + mon_pgdata, + cluster->ssl, + cluster->auth); + + if (ssl_needs_certs(cluster->ssl)) + { + fprintf(f, + "\n" + "[ssl]\n" + "ca_file = " SSL_DIR_IN_CONTAINER "/ca.crt\n" + + /* + * The server cert/key are copied from the read-only bind-mount to + * /var/lib/postgres/ (the volume root, writable by the container + * user) in the start command so the key is owned by the container + * user and PostgreSQL can open it. + */ + "cert_file = /var/lib/postgres/server.crt\n" + "key_file = /var/lib/postgres/server.key\n"); + } + + if (cluster->monitorPassword[0]) + { + fprintf(f, + "\n" + "[pg_auto_failover]\n" + "autoctl_node_password = %s\n", + cluster->monitorPassword); + } + + /* emit [formation ] for every non-default formation */ + for (int fi = 0; fi < cluster->formationCount; fi++) + { + const TestFormation *form = &cluster->formations[fi]; + if (strcmp(form->name, "default") == 0) + { + continue; + } + + /* kind defaults to "ha" — the monitor uses that default when absent */ + fprintf(f, "\n[formation %s]\n", form->name); + } + + fclose(f); + + /* world-writable so the container's docker user can update it at runtime */ + (void) chmod(path, 0666); + log_info("Wrote monitor.ini to \"%s\"", path); + return true; +} + + +/* ----------------------------------------------------------------------- + * compose_gen_write_second_monitor_ini + * ----------------------------------------------------------------------- */ +bool +compose_gen_write_second_monitor_ini(const TestCluster *cluster, const char *dir) +{ + if (!cluster->secondMonitorName[0]) + { + return true; + } + + const char *name = cluster->secondMonitorName; + char path[1024]; + sformat(path, sizeof(path), "%s/%s.ini", dir, name); + + FILE *f = fopen(path, "w"); + if (!f) + { + log_error("Failed to open \"%s\" for writing: %m", path); + return false; + } + + fprintf(f, + "# Generated by pgaftest — do not edit manually\n" + "# Second monitor (replacement): %s\n" + "\n" + "[node]\n" + "kind = monitor\n" + "hostname = %s\n" + "port = 5432\n" + "\n" + "[postgresql]\n" + "pgdata = " NODE_PGDATA "\n" + "\n" + "[options]\n" + "ssl = %s\n" + "auth = %s\n" + "pg_hba_lan = false\n", + name, name, + cluster->ssl, + cluster->auth); + + if (ssl_needs_certs(cluster->ssl)) + { + fprintf(f, + "\n" + "[ssl]\n" + "ca_file = " SSL_DIR_IN_CONTAINER "/ca.crt\n" + + /* + * The server cert/key are copied from the read-only bind-mount to + * /var/lib/postgres/ (the volume root, writable by the container + * user) in the start command so the key is owned by the container + * user and PostgreSQL can open it. + */ + "cert_file = /var/lib/postgres/server.crt\n" + "key_file = /var/lib/postgres/server.key\n"); + } + + fclose(f); + (void) chmod(path, 0666); + log_info("Wrote %s.ini to \"%s\"", name, path); + return true; +} + + +/* ----------------------------------------------------------------------- + * compose_gen_write_node_ini + * ----------------------------------------------------------------------- */ +bool +compose_gen_write_node_ini(const TestCluster *cluster, + const TestFormation *formation, + const TestNode *node, + int nodeId, + const char *dir) +{ + char path[1024]; + sformat(path, sizeof(path), "%s/%s.ini", dir, node->name); + + FILE *f = fopen(path, "w"); + if (!f) + { + log_error("Failed to open \"%s\" for writing: %m", path); + return false; + } + + const char *kindStr; + switch (node->kind) + { + case NODE_KIND_CITUS_COORDINATOR: + { + kindStr = "coordinator"; + break; + } + + case NODE_KIND_CITUS_WORKER: + { + kindStr = "worker"; + break; + } + + default: + { + kindStr = "postgres"; + break; + } + } + + /* Effective ssl/auth: node-level overrides cluster-level default */ + const char *eff_ssl = node->ssl[0] ? node->ssl : cluster->ssl; + const char *eff_auth = node->auth[0] ? node->auth : cluster->auth; + + int pg_port = node->pgPort > 0 ? node->pgPort : 5432; + + fprintf(f, + "# Generated by pgaftest — do not edit manually\n" + "# Node: %s (formation: %s)\n" + "\n" + "[node]\n" + "kind = %s\n" + "name = %s\n" + "hostname = %s\n" + "port = %d\n", + node->name, formation->name, + kindStr, + node->name, + node->name, + pg_port); + + if (node->debianCluster[0]) + { + fprintf(f, "debian_cluster = %s\n", node->debianCluster); + } + + if (node->debianCluster[0]) + { + fprintf(f, + "\n" + "[postgresql]\n" + "pgdata = " DEBIAN_PGDATA_PREFIX "/%s/%s\n" + "\n", + debian_pg_version(), node->debianCluster); + } + else + { + fprintf(f, + "\n" + "[postgresql]\n" + "pgdata = " NODE_PGDATA "\n" + "\n"); + } + + if (node->noMonitor) + { + fprintf(f, "[monitor]\nno_monitor = true\n"); + if (nodeId > 0) + { + fprintf(f, "node_id = %d\n", nodeId); + } + fprintf(f, "\n"); + } + else if (cluster->monitorPassword[0]) + { + fprintf(f, + "[monitor]\n" + "pguri = postgresql://autoctl_node:%s@monitor/pg_auto_failover\n" + "\n", + cluster->monitorPassword); + } + else + { + fprintf(f, + "[monitor]\n" + "pguri = " MONITOR_PGURI "\n" + "\n"); + } + + fprintf(f, + "[formation]\n" + "name = %s\n" + "group = %d\n" + "\n" + "[settings]\n" + "candidate_priority = %d\n" + "replication_quorum = %s\n", + formation->name, + node->group, + node->candidatePriority, + node->replicationQuorum ? "true" : "false"); + + /* Citus role/cluster settings live in their own [citus] section */ + if (node->citusSecondary || node->citusClusterName[0]) + { + fprintf(f, "\n[citus]\n"); + if (node->citusSecondary) + { + fprintf(f, "role = secondary\n"); + } + if (node->citusClusterName[0]) + { + fprintf(f, "cluster_name = %s\n", node->citusClusterName); + } + } + + fprintf(f, + "\n" + "[options]\n" + "ssl = %s\n" + "auth = %s\n" + "pg_hba_lan = %s\n", + eff_ssl, + eff_auth, + node->noMonitor ? "false" : "true"); + + if (ssl_needs_certs(eff_ssl)) + { + fprintf(f, + "\n" + "[ssl]\n" + "ca_file = " SSL_DIR_IN_CONTAINER "/ca.crt\n" + + /* + * The server cert/key are copied from the read-only bind-mount to + * /var/lib/postgres/ (the volume root, writable by the container + * user) in the start command so the key is owned by the container + * user and PostgreSQL can open it. + */ + "cert_file = /var/lib/postgres/server.crt\n" + "key_file = /var/lib/postgres/server.key\n"); + } + + if (node->listen) + { + fprintf(f, "listen = 0.0.0.0\n"); + } + if (node->launchDeferred) + { + fprintf(f, "\n[launch]\nmode = deferred\n"); + } + + /* + * Write pg_auto_failover role passwords when configured. The + * replication.password key already exists in the [replication] section; + * monitor_password and the cluster-level autoctl_node_password go into a + * dedicated [pg_auto_failover] section so that pg_autoctl picks them up + * via the new OPTION_AUTOCTL_NODE_PASSWORD / OPTION_AUTOCTL_MONITOR_PASSWORD + * ini options. + */ + { + const char *eff_monitor_pw = + node->monitorPassword[0] ? node->monitorPassword : ""; + const char *eff_replication_pw = + node->replicationPassword[0] ? node->replicationPassword : ""; + + /* + * cluster->monitorPassword is the autoctl_node password on the monitor. + * It is already embedded in the monitor URI above; it does not belong + * in the node's own [pg_auto_failover] section. + * + * node->monitorPassword is the pgautofailover_monitor health-check role + * password on this data node — set by the monitor when it connects to + * run health checks. + */ + if (eff_replication_pw[0]) + { + fprintf(f, + "\n" + "[replication]\n" + "password = %s\n", + eff_replication_pw); + } + + if (eff_monitor_pw[0]) + { + fprintf(f, + "\n" + "[pg_auto_failover]\n" + "monitor_password = %s\n", + eff_monitor_pw); + } + } + + fclose(f); + (void) chmod(path, 0666); + log_info("Wrote %s.ini to \"%s\"", node->name, path); + return true; +} + + +/* ----------------------------------------------------------------------- + * Helpers used by test_runner.c + * ----------------------------------------------------------------------- */ +void +compose_network_name(const char *projectName, char *buf, int buflen) +{ + snprintf(buf, buflen, "%s_default", projectName); +} + + +void +compose_container_name(const char *projectName, const char *service, + char *buf, int buflen) +{ + snprintf(buf, buflen, "%s-%s-1", projectName, service); +} diff --git a/src/bin/pgaftest/compose_gen.h b/src/bin/pgaftest/compose_gen.h new file mode 100644 index 000000000..17542330b --- /dev/null +++ b/src/bin/pgaftest/compose_gen.h @@ -0,0 +1,70 @@ +/* + * src/bin/pgaftest/compose_gen.h + * Generate a docker-compose.yml from a TestCluster spec. + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the PostgreSQL License. + */ + +#ifndef COMPOSE_GEN_H +#define COMPOSE_GEN_H + +#include +#include "test_spec.h" + +/* + * Generate CA and per-service server certificates in /ssl/ using + * openssl. Only called when cluster->ssl is "verify-ca" or "verify-full". + */ +bool compose_gen_write_ssl_certs(const TestCluster *cluster, + const char *workDir); + +/* + * Write a docker-compose.yml to `path` for the given cluster spec. + * Returns true on success. + */ + +/* cluster is non-const: monitorHostPort is filled in if zero. + * specFile is the absolute path to the .pgaf spec file; it is bind-mounted + * into the pgaftest service at /spec.pgaf. Pass NULL to omit the service. */ +bool compose_gen_write(TestCluster *cluster, + const char *path, + const char *projectName, + const char *contextDir, + const char *specFile); + +/* + * Write a pg_autoctl_node.ini for the second (replacement) monitor into `dir`. + * Only called when cluster->secondMonitorName is set. + */ +bool compose_gen_write_second_monitor_ini(const TestCluster *cluster, + const char *dir); + +/* + * Write a pg_autoctl_node.ini file for the monitor node into `dir`. + * The file is written as /monitor.ini and will be bind-mounted + * into the container at PG_AUTOCTL_NODESPEC_PATH. + */ +bool compose_gen_write_monitor_ini(const TestCluster *cluster, + const char *dir); + +/* + * Write a pg_autoctl_node.ini file for a data node into `dir`. + * The file is written as /name>.ini. + * `formation` supplies the formation name and is written into [formation]. + */ +bool compose_gen_write_node_ini(const TestCluster *cluster, + const TestFormation *formation, + const TestNode *node, + int nodeId, + const char *dir); + +/* Return the docker network name for a project */ +void compose_network_name(const char *projectName, char *buf, int buflen); + +/* Return the container name for a service in a project */ +void compose_container_name(const char *projectName, + const char *service, + char *buf, int buflen); + +#endif /* COMPOSE_GEN_H */ diff --git a/src/bin/pgaftest/main.c b/src/bin/pgaftest/main.c new file mode 100644 index 000000000..a1a90a1ea --- /dev/null +++ b/src/bin/pgaftest/main.c @@ -0,0 +1,54 @@ +/* + * src/bin/pgaftest/main.c + * pgaftest — pg_auto_failover test runner. + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the PostgreSQL License. + */ + +#include +#include +#include + +#include "commandline.h" +#include "defaults.h" +#include "log.h" +#include "lock_utils.h" +#include "monitor_config.h" + +extern CommandLine pgaftest_root; + +/* + * Globals required by shared pg_autoctl source files. + * In pg_autoctl these live in cli_root.c / cli_create_node.c / main.c. + * pgaftest owns stub definitions here so the linker is satisfied. + */ +int pgconnect_timeout = 2; +char *ps_buffer; +size_t ps_buffer_size; +size_t last_status_len; +Semaphore log_semaphore = { 0 }; + +char pg_autoctl_argv0[MAXPGPATH] = "pgaftest"; +char pg_autoctl_program[MAXPGPATH] = "pgaftest"; + +MonitorConfig monitorOptions = { 0 }; + +/* Stub command roots referenced by cli_common.c's keeper_cli_help */ +CommandLine root = make_command("pgaftest", "", "", "", NULL, NULL); +CommandLine root_with_debug = make_command("pgaftest", "", "", "", NULL, NULL); + +int +main(int argc, char **argv) +{ + /* default log level: INFO to stderr */ + log_set_level(LOG_INFO); + + if (argc < 2) + { + commandline_print_usage(&pgaftest_root, stderr); + return 1; + } + + return commandline_run(&pgaftest_root, argc, argv); +} diff --git a/src/bin/pgaftest/pgaftest b/src/bin/pgaftest/pgaftest new file mode 100755 index 0000000000000000000000000000000000000000..f71a9013a5b9dd44c3bbeb4feafaced8c578e819 GIT binary patch literal 870520 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

XdTlq%Bwnm@%Gq1F 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

=F{o$W8OeUg)e%f{(BD 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 literal 0 HcmV?d00001 diff --git a/src/bin/pgaftest/test_runner.c b/src/bin/pgaftest/test_runner.c new file mode 100644 index 000000000..355c3aea5 --- /dev/null +++ b/src/bin/pgaftest/test_runner.c @@ -0,0 +1,3806 @@ +/* + * src/bin/pgaftest/test_runner.c + * Test execution engine for .pgaf specs. + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the PostgreSQL License. + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "log.h" +#include "compose_gen.h" +#include "pgsql.h" +#include "parsing.h" +#include "nodestate_utils.h" +#include "state.h" +#include "test_runner.h" +#include "test_spec.h" + +/* Docker binary name */ +#define DOCKER "docker" + +/* ----------------------------------------------------------------------- + * Forward declarations + * ----------------------------------------------------------------------- */ + +static bool wait_for_state(TestRunner *r, const char *nodeName, + const char *targetState, int timeoutSecs, + bool checkAssigned); +static bool runner_wait_assigned_goal(TestRunner *r, const char *nodeName, + const char *targetState, int timeoutSecs); +static void log_output(const char *prefix, const char *out); + +/* ----------------------------------------------------------------------- + * Internal helpers + * ----------------------------------------------------------------------- */ + +/* Run a shell command, return its exit code */ +static int __attribute__((format(printf, 1, 2))) +run_cmd(const char *fmt, ...) +{ + char cmd[4096]; + va_list ap; + va_start(ap, fmt); + vsnprintf(cmd, sizeof(cmd), fmt, ap); + va_end(ap); + + log_debug("$ %s", cmd); + return system(cmd); +} + + +/* Run a shell command, capture stdout into buf */ +static int __attribute__((format(printf, 3, 4))) +run_cmd_capture(char *buf, int buflen, const char *fmt, ...) +{ + char cmd[4096]; + va_list ap; + va_start(ap, fmt); + vsnprintf(cmd, sizeof(cmd), fmt, ap); + va_end(ap); + + log_debug("$ %s", cmd); + + FILE *p = popen(cmd, "r"); + if (!p) + { + return -1; + } + + int pos = 0; + int c; + while ((c = fgetc(p)) != EOF && pos < buflen - 1) + { + buf[pos++] = (char) c; + } + buf[pos] = '\0'; + + /* trim trailing whitespace */ + while (pos > 0 && (buf[pos - 1] == '\n' || buf[pos - 1] == '\r' || + buf[pos - 1] == ' ')) + { + buf[--pos] = '\0'; + } + + return pclose(p); +} + + +/* ----------------------------------------------------------------------- + * Runner initialisation + * ----------------------------------------------------------------------- */ +static void +runner_init(TestRunner *r, TestSpec *spec, const char *workDir) +{ + memset(r, 0, sizeof(*r)); + r->spec = spec; + + /* + * Project name: inside the compose network COMPOSE_PROJECT_NAME is + * authoritative (the container is invoked with /spec.pgaf which would + * otherwise yield "spec"). On the host, derive from the spec filename. + */ + const char *envProject = getenv("COMPOSE_PROJECT_NAME"); + if (envProject && *envProject) + { + strlcpy(r->projectName, envProject, sizeof(r->projectName)); + } + else + { + const char *base = strrchr(spec->filename, '/'); + base = base ? base + 1 : spec->filename; + strncpy(r->projectName, base, sizeof(r->projectName) - 1); + char *dot = strrchr(r->projectName, '.'); + if (dot) + { + *dot = '\0'; + } + } + + snprintf(r->workDir, sizeof(r->workDir), "%s", workDir); + snprintf(r->composeFile, sizeof(r->composeFile), + "%s/docker-compose.yml", workDir); + + /* + * Inside the compose network the compose file lives on the HOST filesystem + * and is not accessible from the container. Docker Compose v2 can exec + * into a running project by project-name alone; omit -f in that case. + */ + if (getenv("PGAFTEST_COMPOSE_SERVICE")) + { + /* + * Inside the pgaftest container, the compose file lives on the HOST + * at PGAFTEST_HOST_WORK_DIR (bind-mounted to the same path in the + * container so Docker daemon can find it). Use -f to point Docker + * Compose at it so that `up -d` can create new containers. + */ + const char *hostWorkDir = getenv("PGAFTEST_HOST_WORK_DIR"); + if (hostWorkDir && hostWorkDir[0]) + { + snprintf(r->composeBase, sizeof(r->composeBase), + "docker compose -p %s -f %s/docker-compose.yml", + r->projectName, hostWorkDir); + } + else + { + snprintf(r->composeBase, sizeof(r->composeBase), + "docker compose -p %s", r->projectName); + } + } + else + { + snprintf(r->composeBase, sizeof(r->composeBase), + "docker compose -p %s -f %s", r->projectName, r->composeFile); + } + + /* build context = directory from which pgaftest was invoked */ + if (getcwd(r->contextDir, sizeof(r->contextDir)) == NULL) + { + log_error("getcwd failed: %m"); + r->contextDir[0] = '\0'; + } + + /* absolute path to the spec file */ + if (spec->filename[0] == '/') + { + strlcpy(r->specFile, spec->filename, sizeof(r->specFile)); + } + else + { + snprintf(r->specFile, sizeof(r->specFile), "%s/%s", + r->contextDir, spec->filename); + } + + /* Start with the primary monitor as the active target. */ + strlcpy(r->activeMonitorService, "monitor", + sizeof(r->activeMonitorService)); +} + + +/* ----------------------------------------------------------------------- + * TAP output + * ----------------------------------------------------------------------- */ + +/* Append a line to the TAP buffer (does not print yet). */ +static void __attribute__((format(printf, 2, 3))) +tap_buf(TestRunner *r, const char *fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + int n = vsnprintf(r->tapBuffer + r->tapBufferLen, + sizeof(r->tapBuffer) - r->tapBufferLen, + fmt, ap); + va_end(ap); + if (n > 0) + { + r->tapBufferLen += n; + } +} + + +/* Print all buffered TAP output (plan + results) to stdout and flush. */ +static void +tap_plan(TestRunner *r) +{ + printf("1..%d\n", r->tapTotal); + if (r->tapBufferLen > 0) + { + fwrite(r->tapBuffer, 1, r->tapBufferLen, stdout); + } + fflush(stdout); +} + + +static void +tap_ok(TestRunner *r, const char *name) +{ + r->tapPass++; + tap_buf(r, "ok %d - %s\n", r->tapPass + r->tapFail, name); +} + + +static void +tap_not_ok(TestRunner *r, const char *name, const char *reason) +{ + r->tapFail++; + tap_buf(r, "not ok %d - %s\n", r->tapPass + r->tapFail, name); + if (reason) + { + tap_buf(r, "# %s\n", reason); + } +} + + +static void __attribute__((format(printf, 1, 2))) +tap_diag(const char *fmt, ...) +{ + char buf[1024]; + va_list ap; + va_start(ap, fmt); + vsnprintf(buf, sizeof(buf), fmt, ap); + va_end(ap); + printf("# %s\n", buf); + fflush(stdout); +} + + +/* ----------------------------------------------------------------------- + * Compose lifecycle + * ----------------------------------------------------------------------- */ +static bool +runner_compose_generate(TestRunner *r) +{ + /* ensure workdir exists (create intermediate directories as needed) */ + { + char tmp[sizeof(r->workDir)]; + strncpy(tmp, r->workDir, sizeof(tmp) - 1); + for (char *p = tmp + 1; *p; p++) + { + if (*p == '/') + { + *p = '\0'; + (void) mkdir(tmp, 0755); + *p = '/'; + } + } + if (mkdir(r->workDir, 0755) != 0 && errno != EEXIST) + { + log_error("Failed to create work directory \"%s\": %m", r->workDir); + return false; + } + } + + /* Generate SSL certs if the cluster ssl mode requires them (verify-ca/full) */ + if (!compose_gen_write_ssl_certs(&r->spec->cluster, r->workDir)) + { + return false; + } + + /* + * In host mode (no PGAFTEST_COMPOSE_SERVICE) we ARE the test runner, so + * we don't want the compose file to include a pgaftest service — it only + * makes sense for the in-compose CI mode where `docker compose up + * --exit-code-from pgaftest` drives the whole run. Pass NULL to omit it. + */ + const char *specFileForCompose = getenv("PGAFTEST_COMPOSE_SERVICE") + ? r->specFile : NULL; + + if (!compose_gen_write(&r->spec->cluster, + r->composeFile, + r->projectName, + r->contextDir, + specFileForCompose)) + { + return false; + } + + /* + * Write per-node pg_autoctl_node.ini files into the workdir. Each one + * is bind-mounted into its container at /etc/pgaf/node.ini so that + * every container uses the same image and command regardless of role. + */ + if (!compose_gen_write_monitor_ini(&r->spec->cluster, r->workDir)) + { + return false; + } + + if (!compose_gen_write_second_monitor_ini(&r->spec->cluster, r->workDir)) + { + return false; + } + + int globalNodeId = 0; + for (int fi = 0; fi < r->spec->cluster.formationCount; fi++) + { + const TestFormation *form = &r->spec->cluster.formations[fi]; + + for (int ni = 0; ni < form->nodeCount; ni++) + { + if (!compose_gen_write_node_ini(&r->spec->cluster, form, + &form->nodes[ni], + ++globalNodeId, r->workDir)) + { + return false; + } + } + } + + return true; +} + + +static bool +runner_compose_up(TestRunner *r) +{ + log_info("Starting compose stack (project: %s)", r->projectName); + + int rc = run_cmd("%s up --build -d 2>&1", + r->composeBase); + if (rc != 0) + { + log_error("docker compose up failed (exit %d)", rc); + return false; + } + r->composeUp = true; + + /* + * Stop services declared with "launch deferred" (e.g. a second monitor). + * They are defined in the compose file so their image is built and volumes + * are created, but they must not run until the test explicitly starts them. + */ + if (r->spec->cluster.secondMonitorName[0] && + r->spec->cluster.secondMonitorStopped) + { + log_info("Stopping initially-stopped service %s", + r->spec->cluster.secondMonitorName); + rc = run_cmd("%s stop %s 2>&1", + r->composeBase, r->spec->cluster.secondMonitorName); + if (rc != 0) + { + log_error("docker compose stop %s failed (exit %d)", + r->spec->cluster.secondMonitorName, rc); + return false; + } + } + + return true; +} + + +/* + * runner_apply_formation_settings — apply cluster-level formation settings + * that cannot be expressed in the node ini files. + * + * Called once after `docker compose up` and the monitor healthcheck passes. + * Currently applies: + * - number-sync-standbys (when numSync >= 0 in the cluster spec) + * + * We use `docker compose exec` so the call goes through the monitor's own + * pg_autoctl binary and lands on the already-running monitor instance. + */ +static bool +runner_apply_formation_settings(TestRunner *r) +{ + const TestCluster *c = &r->spec->cluster; + bool ok = true; + + for (int fi = 0; fi < c->formationCount; fi++) + { + const TestFormation *form = &c->formations[fi]; + + if (form->numSync < 0) + { + continue; + } + + log_info("Setting formation \"%s\" number-sync-standbys = %d", + form->name, form->numSync); + + int rc = run_cmd( + "%s exec -T monitor " + "pg_autoctl set formation number-sync-standbys %d " + "--pgdata /var/lib/postgres/pgaf --formation %s 2>&1", + r->composeBase, + form->numSync, form->name); + + if (rc != 0) + { + log_error("Failed to set number-sync-standbys for formation " + "\"%s\" (exit %d)", form->name, rc); + ok = false; + } + } + + return ok; +} + + +static bool +runner_compose_down(TestRunner *r) +{ + if (!r->composeUp) + { + return true; + } + + /* + * When pgaftest runs as a service inside the compose stack itself, calling + * "compose down" sends SIGKILL to our own container, so skip it — the host + * runner already calls "compose down" after we exit. + */ + if (getenv("PGAFTEST_COMPOSE_SERVICE")) + { + r->composeUp = false; + return true; + } + + log_info("Tearing down compose stack (project: %s)", r->projectName); + + if (r->notifyConnected) + { + pgsql_finish(&r->notifyConn); + r->notifyConnected = false; + } + + run_cmd("%s down --volumes --remove-orphans 2>&1", + r->composeBase); + r->composeUp = false; + return true; +} + + +/* ----------------------------------------------------------------------- + * State polling via pg_autoctl inspect monitor subcommands + * + * Instead of shelling to psql (with all its quoting pitfalls), we call + * purpose-built subcommands of pg_autoctl that are installed in the + * monitor container: + * + * pg_autoctl inspect monitor node-state --name + * → stdout: "reported|assigned\n" exit 0 on success + * + * pg_autoctl inspect monitor formation-states [--group N] [...] + * → exit 0 when all listed states have ≥1 node, exit 1 when not + * ----------------------------------------------------------------------- */ + +/* + * Run `docker compose exec -T monitor pg_autoctl inspect monitor node-state + * --name ` and parse the "reported|assigned\n" output. + * + * When timeoutSecs > 0, passes --timeout to let the subcommand do its own + * retry loop (exponential back-off with jitter, same policy as the rest of + * pg_autoctl). When targetState is non-NULL, also passes --state so the + * subcommand only exits 0 when the node has reached that reported state. + */ +static bool +monitor_get_node_state(TestRunner *r, const char *nodeName, + char *reported, int replen, + char *assigned, int asslen) +{ + char out[256]; + int rc = run_cmd_capture(out, sizeof(out), + "%s exec -T %s " + "pg_autoctl inspect monitor node-state --name %s 2>/dev/null", + r->composeBase, r->activeMonitorService, nodeName); + + if (rc != 0 || out[0] == '\0') + { + /* + * Fallback for v2.1 monitors that don't have "pg_autoctl inspect": + * query the pgautofailover.node table directly via psql. + */ + int rc2 = run_cmd_capture(out, sizeof(out), + "%s exec -T %s " + "psql -U autoctl_node -d pg_auto_failover -At " + "-c \"SELECT reportedstate||'|'||goalstate FROM pgautofailover.node " + " WHERE nodename='%s'\" 2>/dev/null", + r->composeBase, r->activeMonitorService, nodeName); + if (rc2 != 0 || out[0] == '\0') + { + return false; + } + } + + /* strip trailing whitespace */ + int n = strlen(out); + while (n > 0 && (out[n - 1] == '\n' || out[n - 1] == '\r' || out[n - 1] == ' ')) + { + out[--n] = '\0'; + } + + /* format: reported|goal|health */ + char *sep1 = strchr(out, '|'); + if (!sep1) + { + return false; + } + *sep1 = '\0'; + strncpy(reported, out, replen - 1); + char *sep2 = strchr(sep1 + 1, '|'); + if (sep2) + { + *sep2 = '\0'; + } + strncpy(assigned, sep1 + 1, asslen - 1); + return true; +} + + +/* + * Read the pgdata path for a node from its .ini file in the work directory. + * Returns true and fills pgdata on success; false if the file can't be read. + */ +static bool +get_node_pgdata(TestRunner *r, const char *nodeName, char *pgdata, int len) +{ + char iniPath[1280]; + snprintf(iniPath, sizeof(iniPath), "%s/%s.ini", r->workDir, nodeName); + + FILE *f = fopen(iniPath, "r"); + if (!f) + { + return false; + } + + char line[256]; + bool found = false; + + while (fgets(line, sizeof(line), f)) + { + if (strncmp(line, "pgdata", 6) == 0) + { + char *eq = strchr(line, '='); + if (eq) + { + eq++; + while (*eq == ' ' || *eq == '\t') + { + eq++; + } + char *end = eq + strlen(eq) - 1; + while (end > eq && (*end == '\n' || *end == '\r' || *end == ' ')) + { + *end-- = '\0'; + } + strlcpy(pgdata, eq, len); + found = true; + break; + } + } + } + fclose(f); + return found; +} + + +/* + * Query the monitor for reported|goal|health of a node in one call. + * Returns true if the node was found; health is set to the integer value + * from pgautofailover.node.health (1 = healthy, -1 = unhealthy/unknown). + */ +static bool +monitor_get_node_health(TestRunner *r, const char *nodeName, + char *reported, int replen, + char *goal, int goallen, + int *health) +{ + char out[256]; + int rc = run_cmd_capture(out, sizeof(out), + "%s exec -T monitor " + "pg_autoctl inspect monitor node-state --name %s 2>/dev/null", + r->composeBase, nodeName); + + if (rc != 0 || out[0] == '\0') + { + return false; + } + + int n = strlen(out); + while (n > 0 && (out[n - 1] == '\n' || out[n - 1] == '\r' || out[n - 1] == ' ')) + { + out[--n] = '\0'; + } + + /* format: reported|goal|health */ + char *p1 = strchr(out, '|'); + if (!p1) + { + return false; + } + *p1 = '\0'; + strlcpy(reported, out, replen); + + char *p2 = strchr(p1 + 1, '|'); + if (!p2) + { + return false; + } + *p2 = '\0'; + strlcpy(goal, p1 + 1, goallen); + + *health = atoi(p2 + 1); + return true; +} + + +/* + * Fetch and log all node states from the monitor via pg_autoctl show state. + * Used for progress snapshots during wait loops. + */ +static void +log_formation_state(TestRunner *r) +{ + char out[4096]; + int rc = run_cmd_capture(out, sizeof(out), + "%s exec -T monitor " + "pg_autoctl show state 2>/dev/null", + r->composeBase); + + if (rc != 0 || out[0] == '\0') + { + return; + } + + char *line = out; + while (*line) + { + char *nl = strchr(line, '\n'); + if (nl) + { + *nl = '\0'; + } + if (*line) + { + log_info(" %s", line); + } + if (!nl) + { + break; + } + line = nl + 1; + } +} + + +/* ----------------------------------------------------------------------- + * Direct LISTEN/NOTIFY connection to the monitor postgres + * + * compose_gen exposes the monitor's postgres on a random host port. + * We connect directly from the host to receive state-change notifications + * in real time, replacing the polling-via-docker-exec approach. + * ----------------------------------------------------------------------- */ + +/* + * Return the host port for the named monitor service, or 0 if unknown. + * Used to build the direct libpq connection string for LISTEN/NOTIFY and + * to look up the correct service name for monitor_get_node_state(). + */ +static int +runner_monitor_host_port(TestRunner *r, const char *svc) +{ + const TestCluster *cl = &r->spec->cluster; + + if (strcmp(svc, "monitor") == 0) + { + return cl->monitorHostPort; + } + + if (cl->secondMonitorName[0] && strcmp(svc, cl->secondMonitorName) == 0) + { + return cl->secondMonitorHostPort; + } + + return 0; +} + + +/* + * Open (or reopen) a direct libpq connection to the monitor and LISTEN on + * the "state" channel. Safe to call multiple times — no-op if already + * connected. + */ +static bool +runner_notify_connect(TestRunner *r) +{ + if (r->notifyConnected && + r->notifyConn.connection != NULL && + PQstatus(r->notifyConn.connection) == CONNECTION_OK) + { + return true; + } + + /* close any stale connection */ + if (r->notifyConn.connection) + { + pgsql_finish(&r->notifyConn); + r->notifyConnected = false; + } + + TestCluster *cl = &r->spec->cluster; + const char *svc = r->activeMonitorService; + + char connstr[512]; + + if (getenv("PGAFTEST_COMPOSE_SERVICE")) + { + /* + * Inside the compose network: connect directly via the monitor service + * hostname. Include the password when the cluster uses md5/scram auth + * so that the autoctl_node role can authenticate. + */ + if (cl->monitorPassword[0]) + { + snprintf(connstr, sizeof(connstr), + "host=%s port=5432 dbname=pg_auto_failover " + "user=autoctl_node password=%s connect_timeout=5", + svc, cl->monitorPassword); + } + else + { + snprintf(connstr, sizeof(connstr), + "host=%s port=5432 dbname=pg_auto_failover " + "user=autoctl_node connect_timeout=5", + svc); + } + } + else + { + int port = runner_monitor_host_port(r, svc); + if (port == 0) + { + log_debug("host port for monitor service \"%s\" not set, " + "skipping LISTEN connection", svc); + return false; + } + if (cl->monitorPassword[0]) + { + snprintf(connstr, sizeof(connstr), + "host=localhost port=%d dbname=pg_auto_failover " + "user=autoctl_node password=%s " + "connect_timeout=5", + port, cl->monitorPassword); + } + else + { + snprintf(connstr, sizeof(connstr), + "host=localhost port=%d dbname=pg_auto_failover " + "user=autoctl_node " + "connect_timeout=2", + port); + } + } + + strlcpy(r->notifyConn.connectionString, connstr, + sizeof(r->notifyConn.connectionString)); + + /* + * Suppress libpq's "could not connect" ERROR logs while we're polling — + * connection failures are expected until the monitor is ready. + * Restore the original level immediately after the attempt. + */ + int savedLevel = log_get_level(); + log_set_level(LOG_FATAL); + + char *channels[] = { "state", NULL }; + bool ok = pgsql_listen(&r->notifyConn, channels); + + log_set_level(savedLevel); + + if (!ok) + { + log_debug("LISTEN connection to monitor service \"%s\" failed " + "(will retry)", svc); + return false; + } + + log_debug("Listening on monitor service \"%s\" for state notifications", + svc); + r->notifyConnected = true; + return true; +} + + +/* + * Drain pending notifications from the monitor connection, logging each one. + * + * If markNodes/markStates are provided (count > 0), any notification where + * both goalState AND reportedState equal the target state for the matching + * node is printed with a "* [notify]" prefix. Only the convergence event + * (the notification that actually lifts the wait) is marked; the earlier + * assignment event (goalState matches but reportedState doesn't yet) is + * printed with the ordinary " [notify]" prefix. + * + * Callers that have no current wait condition pass count=0 (or NULL arrays). + * Returns the last CurrentNodeState parsed, or leaves *last unchanged if none. + */ +static void +runner_drain_notify(TestRunner *r, CurrentNodeState *last, + const char *const *markNodes, + const char *const *markStates, + int markCount, + bool *satisfied) /* optional: set [i] on convergence */ +{ + PGconn *conn = r->notifyConn.connection; + if (!conn) + { + return; + } + + PQconsumeInput(conn); + + PGnotify *notify; + while ((notify = PQnotifies(conn)) != NULL) + { + if (strcmp(notify->relname, "state") == 0) + { + CurrentNodeState ns = { 0 }; + if (parse_state_notification_message(&ns, notify->extra)) + { + const char *ns_goal = NodeStateToString(ns.goalState); + const char *ns_rep = NodeStateToString(ns.reportedState); + + /* + * Mark '*' only when this notification IS the convergence event: + * both goalState and reportedState equal the target for this node. + * markNodes[i] == NULL is a wildcard matching any node. + */ + bool matched = false; + for (int i = 0; i < markCount; i++) + { + if (markStates && markStates[i] && + strcmp(ns_goal, markStates[i]) == 0 && + strcmp(ns_rep, markStates[i]) == 0 && + (!markNodes || !markNodes[i] || + strcmp(ns.node.name, markNodes[i]) == 0)) + { + matched = true; + if (satisfied) + { + satisfied[i] = true; + } + } + } + const char *prefix = matched ? "* [notify]" : " [notify]"; + + if (ns.health >= 0) + { + log_info("%s %s: %s \xe2\x9e\x9c %s", + prefix, ns.node.name, ns_rep, ns_goal); + } + else + { + log_info("%s %s: %s \xe2\x9e\x9c %s (unhealthy)", + prefix, ns.node.name, ns_rep, ns_goal); + } + if (last) + { + *last = ns; + } + } + else + { + log_debug("unparseable state notification: %s", notify->extra); + } + } + PQfreemem(notify); + } +} + + +/* + * Wait on the monitor socket with select(), up to `remainMs` milliseconds. + * Returns true if there is data to read, false on timeout. + */ +static bool +runner_wait_socket(TestRunner *r, int remainMs) +{ + PGconn *conn = r->notifyConn.connection; + if (!conn) + { + return false; + } + + int sock = PQsocket(conn); + if (sock < 0) + { + return false; + } + + fd_set rfds; + FD_ZERO(&rfds); + FD_SET(sock, &rfds); + + struct timeval tv = { + .tv_sec = remainMs / 1000, + .tv_usec = (remainMs % 1000) * 1000, + }; + bool ready = select(sock + 1, &rfds, NULL, NULL, &tv) > 0; + if (ready) + { + PQconsumeInput(conn); /* pull data into libpq buffer so PQnotifies sees it */ + } + return ready; +} + + +/* + * Check that the formation has converged: for each required state, at least + * one cluster node must have both reportedstate = assignedstate = that state. + * This is the correct convergence condition — checking only assignedstate + * fires as soon as the monitor makes an assignment, before nodes confirm. + * + * groupIds / groupCount optionally restrict the check to specific Citus groups + * (pass NULL / 0 for a plain HA formation). + */ +static bool +monitor_check_formation_converged(TestRunner *r, + const char (*states)[64], int stateCount, + const int *groupIds, int groupCount) +{ + bool satisfied[PGAF_MAX_WAIT_STATES] = { false }; + int nodesQueried = 0; + + const TestCluster *c = &r->spec->cluster; + for (int fi = 0; fi < c->formationCount; fi++) + { + const TestFormation *form = &c->formations[fi]; + for (int ni = 0; ni < form->nodeCount; ni++) + { + const TestNode *node = &form->nodes[ni]; + + /* skip nodes not in the requested groups */ + if (groupCount > 0) + { + bool inGroup = false; + for (int gi = 0; gi < groupCount; gi++) + { + if (node->group == groupIds[gi]) + { + inGroup = true; + break; + } + } + if (!inGroup) + { + continue; + } + } + + char reported[64] = "", assigned[64] = ""; + if (!monitor_get_node_state(r, node->name, + reported, sizeof(reported), + assigned, sizeof(assigned))) + { + continue; + } + + nodesQueried++; + for (int si = 0; si < stateCount && si < PGAF_MAX_WAIT_STATES; si++) + { + if (strcmp(reported, states[si]) == 0 && + strcmp(assigned, states[si]) == 0) + { + satisfied[si] = true; + } + } + } + } + + /* + * If no nodes could be reached via subprocess (e.g. monitor running v2.1 + * which lacks "pg_autoctl inspect monitor node-state"), the check is + * inconclusive rather than negative. Return true so the caller can trust + * the LISTEN-based satisfied[] from runner_drain_notify instead. + */ + if (nodesQueried == 0) + { + return true; + } + + for (int si = 0; si < stateCount; si++) + { + if (!satisfied[si]) + { + return false; + } + } + return true; +} + + +/* + * Wait until the formation has at least one node in each listed state, where + * both reportedstate and assignedstate have converged on the target. + * + * Strategy: + * 1. Fast-path subprocess check: if the formation is already converged on + * entry (e.g. after a synchronous command), return immediately. + * 2. LISTEN-driven main loop: block on the notify socket, drain each batch + * of notifications, and track which target states have been seen with + * BOTH goalState and reportedState matching (convergence events) via + * the satisfied[] array passed to runner_drain_notify. + * When all target states are satisfied by notifications, do ONE subprocess + * double-check before declaring success — guards against a rare race where + * a node races through the target state and moves on before we verify. + * 3. No-LISTEN fallback: if the notify connection is unavailable, fall back + * to subprocess polling every second. + */ +static bool +monitor_wait_formation_states(TestRunner *r, + const char (*states)[64], int stateCount, + const int *groupIds, int groupCount, + int timeoutSecs) +{ + runner_notify_connect(r); + + /* pointer arrays for drain marking — NULL node = wildcard (any node) */ + const char *ms[PGAF_MAX_WAIT_STATES]; + for (int i = 0; i < stateCount && i < PGAF_MAX_WAIT_STATES; i++) + { + ms[i] = states[i]; + } + + /* satisfied[i] is set by runner_drain_notify when a convergence + * notification is seen for states[i] (both goal and reported match) */ + bool satisfied[PGAF_MAX_WAIT_STATES] = { false }; + + /* + * Drain any notifications already buffered in libpq before the fast-path + * check. This is important for synchronous commands like perform failover: + * the entire FSM cycle completes inside the blocking exec, so all + * intermediate NOTIFY messages have accumulated in the socket by the time + * we arrive here. Draining first ensures those events appear in the + * current step's log output (with '*' marks on the convergence ones) rather + * than spilling into the next command's inter-drain or into teardown. + */ + if (r->notifyConnected) + { + runner_drain_notify(r, NULL, NULL, ms, stateCount, satisfied); + } + + /* fast-path: already converged (confirmed by subprocess) */ + bool allSatisfiedEarly = true; + for (int i = 0; i < stateCount; i++) + { + if (!satisfied[i]) + { + allSatisfiedEarly = false; + break; + } + } + + if (allSatisfiedEarly && + monitor_check_formation_converged(r, states, stateCount, + groupIds, groupCount)) + { + return true; + } + + time_t deadline = time(NULL) + timeoutSecs; + int pollCounter = 0; /* counts 5s wait-socket cycles */ + + while (time(NULL) < deadline) + { + int remainMs = (int) ((deadline - time(NULL)) * 1000); + if (remainMs <= 0) + { + break; + } + + if (r->notifyConnected) + { + /* block until a notification arrives or up to 5s */ + int waitMs = remainMs < 5000 ? remainMs : 5000; + runner_wait_socket(r, waitMs); + + /* drain: logs, marks '*', and sets satisfied[i] on convergence */ + runner_drain_notify(r, NULL, NULL, ms, stateCount, satisfied); + + /* when every target state has been seen converged via notification, + * do one subprocess double-check before declaring success */ + bool allSatisfied = true; + for (int i = 0; i < stateCount; i++) + { + if (!satisfied[i]) + { + allSatisfied = false; + break; + } + } + + /* + * Also poll the monitor directly every ~5 cycles (~25 s) even + * without allSatisfied. This handles the case where the LISTEN + * connection reconnects AFTER the state transitions fired — those + * notifications are gone and allSatisfied will never become true, + * but the monitor's current-state query still reflects reality. + */ + ++pollCounter; + + /* + * When notifications say we converged, trust them — return + * immediately without a subprocess double-check. The double-check + * races with fast post-convergence transitions: by the time the + * subprocess queries the monitor the nodes may have already moved + * on, producing a false negative that causes a 120s timeout. + * + * The periodic (every ~25s) subprocess poll remains as a fallback + * for missed notifications (e.g. the LISTEN reconnect after a + * monitor Postgres restart in the upgrade test). + */ + if (allSatisfied) + { + return true; + } + if (pollCounter % 5 == 0) + { + if (monitor_check_formation_converged(r, states, stateCount, + groupIds, groupCount)) + { + return true; + } + } + + runner_notify_connect(r); /* reconnect if socket went bad */ + } + else + { + /* no LISTEN — subprocess poll once per second */ + pg_usleep(1000 * 1000); + runner_notify_connect(r); + if (monitor_check_formation_converged(r, states, stateCount, + groupIds, groupCount)) + { + return true; + } + } + } + + /* deadline reached: one final drain + subprocess check */ + if (r->notifyConnected) + { + runner_drain_notify(r, NULL, NULL, ms, stateCount, satisfied); + } + if (monitor_check_formation_converged(r, states, stateCount, + groupIds, groupCount)) + { + return true; + } + + log_formation_state(r); + return false; +} + + +/* + * Wait until the formation has at least one node in each of the requested + * states (within the given groups), or timeout expires. + */ +static bool +wait_for_states(TestRunner *r, TestCmd *cmd) +{ + /* build a human-readable label for log messages */ + char label[256] = ""; + for (int i = 0; i < cmd->waitStateCount; i++) + { + if (i > 0) + { + strncat(label, ", ", sizeof(label) - strlen(label) - 1); + } + strncat(label, cmd->waitStates[i], sizeof(label) - strlen(label) - 1); + } + + if (cmd->waitGroupCount > 0) + { + strncat(label, " in group(s) ", sizeof(label) - strlen(label) - 1); + for (int i = 0; i < cmd->waitGroupCount; i++) + { + char g[16]; + snprintf(g, sizeof(g), "%s%d", i > 0 ? "," : "", cmd->waitGroups[i]); + strncat(label, g, sizeof(label) - strlen(label) - 1); + } + } + + time_t t0 = time(NULL); + if (!monitor_wait_formation_states(r, + (const char (*)[64])cmd->waitStates, + cmd->waitStateCount, + cmd->waitGroups, + cmd->waitGroupCount, + cmd->timeoutSeconds)) + { + log_error("Timed out after %ds waiting for formation states: %s", + (int) (time(NULL) - t0), label); + return false; + } + return true; +} + + +/* + * For a single node: query the monitor for its current group and whether it + * is already the primary of that group. If not, call + * pg_autoctl perform promotion --name + * on the monitor and wait until it becomes primary. + * + * pg_autoctl perform promotion is synchronous: it returns only after the new + * primary has reached the 'primary' state. We still poll to confirm the + * monitor agrees, using the existing wait_for_state() helper. + */ +static bool +runner_promote_one(TestRunner *r, const char *nodeName) +{ + /* Check current state */ + char reported[64] = "", assigned[64] = ""; + if (!monitor_get_node_state(r, nodeName, + reported, sizeof(reported), + assigned, sizeof(assigned))) + { + log_error("promote: cannot query monitor for node %s", nodeName); + return false; + } + + if (strcmp(reported, "primary") == 0) + { + log_info(" promote: %s is already primary — no-op", nodeName); + return true; + } + + log_info(" promote: %s is %s, requesting promotion", nodeName, reported); + + /* find the formation this node belongs to */ + const char *formation = "default"; + const TestCluster *c = &r->spec->cluster; + for (int fi = 0; fi < c->formationCount; fi++) + { + const TestFormation *form = &c->formations[fi]; + for (int ni = 0; ni < form->nodeCount; ni++) + { + if (strcmp(form->nodes[ni].name, nodeName) == 0) + { + formation = form->name; + break; + } + } + } + + char out[4096] = ""; + int rc = run_cmd_capture(out, sizeof(out), + "%s exec -T monitor " + "pg_autoctl perform promotion --name %s --formation %s 2>&1", + r->composeBase, nodeName, formation); + + if (rc != 0) + { + if (out[0]) + { + log_output(" ", out); + } + log_error("promote: pg_autoctl perform promotion --name %s " + "--formation %s failed (exit %d)", nodeName, formation, rc); + return false; + } + + /* confirm monitor agrees */ + return wait_for_state(r, nodeName, "primary", PGAF_TIMEOUT_DEFAULT, false); +} + + +/* + * runner_wait_notify_goal — LISTEN-driven single-node wait (items 5 & 6). + * + * Blocks until a "state" notification arrives where both states have + * converged on the target: + * ns.node.name == nodeName + * AND NodeStateToString(ns.goalState) == targetState + * AND NodeStateToString(ns.reportedState) == targetState + * + * The '*' marker is applied as soon as goalState matches (monitor has + * assigned the target) even before the node confirms, so the log shows + * the assignment event clearly. The wait only lifts when the node + * confirms (reportedState also equals target), verified by a final SQL + * check that both assigned_state and reportedState agree. + * + * Using goalState (== monitor's assigned_state) as the trigger is faster and + * more reliable than polling reportedState: the notification fires the moment + * the monitor makes the assignment decision, before the node has transitioned. + * + * Along the way, any intermediate goalState values for this node are checked + * against passThroughStates[] and seenThrough[] is updated (item 6). + * + * After the target goalState notification arrives: + * - drain remaining notifications (log them all) + * - final SQL check: assigned_state == targetState via monitor_get_node_state() + * - if SQL disagrees (rare race), keep looping + * + * Fallback when LISTEN is unavailable: poll assigned_state via SQL every 500ms; + * track pass-through states from reportedState. + * + * For nodes the monitor has marked unhealthy, also accepts a match from the + * node's local FSM (pg_autoctl inspect fsm node-state) to handle cases where + * the keeper self-assigns a state without monitor contact. + */ +static bool +runner_wait_notify_goal(TestRunner *r, + const char *nodeName, + const char *targetState, + const char passThroughStates[][64], + bool *seenThrough, + int passThroughCount, + int timeoutSecs) +{ + runner_notify_connect(r); + + /* + * Fast path: if the node has already converged on the target state + * (reportedState == assignedState == targetState), return immediately. + * Without this, a node that settled during a preceding command would + * spin the full timeout waiting for a notification that won't arrive. + */ + { + char rep0[64] = "", asgn0[64] = ""; + if (monitor_get_node_state(r, nodeName, rep0, sizeof(rep0), + asgn0, sizeof(asgn0)) && + strcmp(rep0, targetState) == 0 && + strcmp(asgn0, targetState) == 0) + { + return true; + } + } + + time_t deadline = time(NULL) + timeoutSecs; + + while (time(NULL) < deadline) + { + if (r->notifyConnected) + { + PQconsumeInput(r->notifyConn.connection); + + PGnotify *notify; + while ((notify = PQnotifies(r->notifyConn.connection)) != NULL) + { + if (strcmp(notify->relname, "state") == 0) + { + CurrentNodeState ns = { 0 }; + if (parse_state_notification_message(&ns, notify->extra)) + { + const char *ns_goal = NodeStateToString(ns.goalState); + const char *ns_rep = NodeStateToString(ns.reportedState); + + /* + * Mark '*' only when this IS the convergence notification: + * both goalState and reportedState equal the target. + * The earlier assignment notification (goalState matches + * but reportedState doesn't yet) prints without a marker. + */ + bool goal_matched = (strcmp(ns.node.name, nodeName) == 0 && + strcmp(ns_goal, targetState) == 0 && + strcmp(ns_rep, targetState) == 0); + const char *prefix = goal_matched ? "* [notify]" : " [notify]"; + + if (ns.health >= 0) + { + log_info("%s %s: %s \xe2\x9e\x9c %s", + prefix, ns.node.name, ns_rep, ns_goal); + } + else + { + log_info("%s %s: %s \xe2\x9e\x9c %s (unhealthy)", + prefix, ns.node.name, ns_rep, ns_goal); + } + + if (strcmp(ns.node.name, nodeName) == 0) + { + /* track pass-through states from goalState (item 6) */ + for (int i = 0; i < passThroughCount; i++) + { + if (seenThrough && !seenThrough[i] && + strcmp(ns_goal, passThroughStates[i]) == 0) + { + log_info("wait %s: observed pass-through " + "assigned-state %s", + nodeName, ns_goal); + seenThrough[i] = true; + } + } + + /* + * Lift the wait only when both states have converged: + * the monitor assigned the target (goalState) and the + * node confirmed it (reportedState). The notification + * already carries both values so we can test here + * without an extra round-trip; we still do a final SQL + * confirmation to guard against a torn read. + */ + if (strcmp(ns_goal, targetState) == 0 && + strcmp(ns_rep, targetState) == 0) + { + PQfreemem(notify); + + /* post-match drain: no marking — belongs to next wait */ + runner_drain_notify(r, NULL, NULL, NULL, 0, NULL); + + /* final SQL: confirm both assigned and reported */ + char rep[64] = "", asgn[64] = ""; + if (monitor_get_node_state(r, nodeName, + rep, sizeof(rep), + asgn, sizeof(asgn))) + { + if (strcmp(rep, targetState) == 0 && + strcmp(asgn, targetState) == 0) + { + return true; + } + + /* + * Rare torn read: keep looping. + * The next notification will re-trigger. + */ + log_debug(" %s: notify shows convergence on %s " + "but SQL has rep=%s asgn=%s, retrying", + nodeName, targetState, rep, asgn); + } + else + { + /* can't reach monitor — trust the notification */ + return true; + } + goto next_iter; + } + } + } + else + { + log_debug("unparseable state notification: %s", + notify->extra); + } + } + PQfreemem(notify); + } + + /* + * For unhealthy nodes: also accept a match from the node's local + * FSM — the keeper self-assigns certain states without monitor + * contact. + * + * For nodes that have been hard-killed (compose kill), the + * container is gone so exec fails. Accept goalState-alone match + * for unhealthy nodes: a dead node can never report the new state, + * but the monitor has already made the assignment decision. + */ + { + char rep[64] = "", goal[64] = ""; + int health = 1; + if (monitor_get_node_health(r, nodeName, + rep, sizeof(rep), + goal, sizeof(goal), + &health) && + health <= 0) + { + if (strcmp(goal, targetState) == 0) + { + return true; + } + + char pgdata[1024] = ""; + get_node_pgdata(r, nodeName, pgdata, sizeof(pgdata)); + char out[256]; + if (run_cmd_capture(out, sizeof(out), + "%s exec -T %s " + "pg_autoctl inspect fsm node-state" + " --state %s --timeout 0%s%s 2>/dev/null", + r->composeBase, nodeName, targetState, + pgdata[0] ? " --pgdata " : "", + pgdata[0] ? pgdata : "") == 0) + { + return true; + } + } + } + } + else + { + /* + * No LISTEN connection — fall back to polling via SQL. + * Require both reportedState and assignedState to equal the target + * (same convergence criterion as the notification path), except for + * unhealthy nodes where goalState alone is accepted (dead nodes + * can never report the new state). + */ + char rep[64] = "", asgn[64] = ""; + if (monitor_get_node_state(r, nodeName, rep, sizeof(rep), + asgn, sizeof(asgn))) + { + for (int i = 0; i < passThroughCount; i++) + { + if (seenThrough && !seenThrough[i] && + strcmp(rep, passThroughStates[i]) == 0) + { + log_info("wait %s: observed pass-through state %s", + nodeName, rep); + seenThrough[i] = true; + } + } + if (strcmp(rep, targetState) == 0 && + strcmp(asgn, targetState) == 0) + { + return true; + } + + /* unhealthy node: accept goalState match alone */ + char rep2[64] = "", goal2[64] = ""; + int health2 = 1; + if (strcmp(asgn, targetState) == 0 && + monitor_get_node_health(r, nodeName, + rep2, sizeof(rep2), + goal2, sizeof(goal2), + &health2) && + health2 <= 0) + { + return true; + } + } + } + +next_iter: + { + int remainMs = (int) ((deadline - time(NULL)) * 1000); + if (remainMs <= 0) + { + break; + } + int waitMs = remainMs < 2000 ? remainMs : 2000; + + if (r->notifyConnected) + { + runner_wait_socket(r, waitMs); + runner_notify_connect(r); + } + else + { + pg_usleep(500 * 1000); + runner_notify_connect(r); + } + } + } + + /* one last chance: drain + SQL check — mark the target in case it just arrived */ + if (r->notifyConnected) + { + runner_drain_notify(r, NULL, &nodeName, &targetState, 1, NULL); + } + + char rep2[64] = "", asgn2[64] = ""; + if (monitor_get_node_state(r, nodeName, rep2, sizeof(rep2), + asgn2, sizeof(asgn2)) && + strcmp(rep2, targetState) == 0 && + strcmp(asgn2, targetState) == 0) + { + return true; + } + + return false; +} + + +/* + * runner_wait_assigned_goal — wait until the monitor's goalState for nodeName + * equals targetState, without requiring reportedState to converge. + * + * This implements the semantics of "wait until assigned-state = X": + * succeed as soon as the monitor assigns X as the goalState, even if the node + * has not yet transitioned its own reportedState. This matches the Python + * test's wait_until_assigned_state() behaviour. + * + * Uses NOTIFY fast-path (goalState match alone) and falls back to polling + * the assigned_state column directly via monitor_get_node_state(). + */ +static bool +runner_wait_assigned_goal(TestRunner *r, const char *nodeName, + const char *targetState, int timeoutSecs) +{ + runner_notify_connect(r); + + /* fast path: already there */ + { + char rep[64] = "", asgn[64] = ""; + if (monitor_get_node_state(r, nodeName, rep, sizeof(rep), + asgn, sizeof(asgn)) && + strcmp(asgn, targetState) == 0) + { + return true; + } + } + + time_t deadline = time(NULL) + timeoutSecs; + + while (time(NULL) < deadline) + { + if (r->notifyConnected) + { + PQconsumeInput(r->notifyConn.connection); + + PGnotify *notify; + while ((notify = PQnotifies(r->notifyConn.connection)) != NULL) + { + if (strcmp(notify->relname, "state") == 0) + { + CurrentNodeState ns = { 0 }; + if (parse_state_notification_message(&ns, notify->extra)) + { + const char *ns_goal = NodeStateToString(ns.goalState); + const char *ns_rep = NodeStateToString(ns.reportedState); + + if (ns.health >= 0) + { + log_info(" [notify] %s: %s \xe2\x9e\x9c %s", + ns.node.name, ns_rep, ns_goal); + } + else + { + log_info(" [notify] %s: %s \xe2\x9e\x9c %s (unhealthy)", + ns.node.name, ns_rep, ns_goal); + } + + if (strcmp(ns.node.name, nodeName) == 0 && + strcmp(ns_goal, targetState) == 0) + { + PQfreemem(notify); + + /* confirm via SQL */ + char rep[64] = "", asgn[64] = ""; + if (monitor_get_node_state(r, nodeName, + rep, sizeof(rep), + asgn, sizeof(asgn))) + { + if (strcmp(asgn, targetState) == 0) + { + return true; + } + + /* rare race: goalState changed already; keep looping */ + } + else + { + return true; /* can't reach monitor, trust notify */ + } + goto next_iter_asgn; + } + } + } + PQfreemem(notify); + } + } + else + { + /* fallback: poll assigned_state via SQL */ + char rep[64] = "", asgn[64] = ""; + if (monitor_get_node_state(r, nodeName, rep, sizeof(rep), + asgn, sizeof(asgn)) && + strcmp(asgn, targetState) == 0) + { + return true; + } + } + +next_iter_asgn: + { + int remainMs = (int) ((deadline - time(NULL)) * 1000); + if (remainMs <= 0) + { + break; + } + int waitMs = remainMs < 2000 ? remainMs : 2000; + + if (r->notifyConnected) + { + runner_wait_socket(r, waitMs); + } + else + { + pg_usleep(500 * 1000); + } + + runner_notify_connect(r); + } + } + + return false; +} + + +/* + * Wait until the node reaches the target state, or timeout expires. + * + * For state (reported) checks: delegates to runner_wait_notify_goal() which + * waits for both goalState and reportedState to converge on target. + * For assigned-state checks: delegates to runner_wait_assigned_goal() which + * succeeds as soon as the monitor's goalState equals target. + */ +static bool +wait_for_state(TestRunner *r, const char *nodeName, const char *targetState, + int timeoutSecs, bool checkAssigned) +{ + if (checkAssigned) + { + /* + * assigned-state check: succeed as soon as the monitor's goalState + * equals the target, without requiring the node to have transitioned + * its reportedState. This matches the Python test's behaviour for + * wait_until_assigned_state() and is necessary for cases like + * test_015_003 where the primary is assigned "primary" goalstate but + * cannot complete the transition until standbys reconnect. + */ + if (!runner_wait_assigned_goal(r, nodeName, targetState, timeoutSecs)) + { + log_error("Timeout waiting for %s assigned-state = %s", + nodeName, targetState); + return false; + } + return true; + } + + if (!runner_wait_notify_goal(r, nodeName, targetState, + NULL, NULL, 0, timeoutSecs)) + { + log_error("Timeout waiting for %s state = %s", + nodeName, targetState); + return false; + } + return true; +} + + +/* + * runner_check_stays_notify — drain pending notifications and verify none + * changed the watched node's goalState away from expectedState (item 9). + * + * Returns true if the state is still as expected (no disruptive notification). + * Returns false and writes into errBuf if a state-change notification arrived. + */ +static bool +runner_check_stays_notify(TestRunner *r, + const char *nodeName, const char *expectedState, + char *errBuf, int errLen) +{ + if (!r->notifyConnected) + { + return true; /* no LISTEN — caller will poll instead */ + } + PQconsumeInput(r->notifyConn.connection); + + PGnotify *notify; + while ((notify = PQnotifies(r->notifyConn.connection)) != NULL) + { + if (strcmp(notify->relname, "state") == 0) + { + CurrentNodeState ns = { 0 }; + if (parse_state_notification_message(&ns, notify->extra)) + { + const char *ns_goal = NodeStateToString(ns.goalState); + const char *ns_rep = NodeStateToString(ns.reportedState); + + if (ns.health >= 0) + { + log_info(" [notify] %s: %s \xe2\x9e\x9c %s", + ns.node.name, ns_rep, ns_goal); + } + else + { + log_info(" [notify] %s: %s \xe2\x9e\x9c %s (unhealthy)", + ns.node.name, ns_rep, ns_goal); + } + + if (strcmp(ns.node.name, nodeName) == 0 && + strcmp(ns_goal, expectedState) != 0) + { + PQfreemem(notify); + snprintf(errBuf, errLen, + "stays-while: %s was assigned state %s " + "(expected to stay %s)", + nodeName, ns_goal, expectedState); + return false; + } + } + } + PQfreemem(notify); + } + return true; +} + + +/* ----------------------------------------------------------------------- + * SQL execution on a service + * ----------------------------------------------------------------------- */ + +/* + * escape_sql_for_shell — escape single-quotes for embedding SQL in a + * single-quoted shell argument using the '\'' trick. + */ +static void +escape_sql_for_shell(const char *sql, char *out, int outlen) +{ + int oi = 0; + for (int i = 0; sql[i] && oi < outlen - 2; i++) + { + if (sql[i] == '\'') + { + out[oi++] = '\''; + out[oi++] = '\\'; + out[oi++] = '\''; + if (oi < outlen - 1) + { + out[oi++] = '\''; + } + } + else + { + out[oi++] = sql[i]; + } + } + out[oi] = '\0'; +} + + +/* + * parse_sqlstate — scan output for a PostgreSQL SQLSTATE code. + * + * With VERBOSITY=verbose, psql formats errors as: + * ERROR: XXXXX: message text + * where XXXXX is a 5-character alphanumeric SQLSTATE code. + * Writes up to 6 bytes (5 + NUL) into state. + */ +static void +parse_sqlstate(const char *output, char *state, int statelen) +{ + state[0] = '\0'; + const char *p = output; + while (p && *p) + { + p = strstr(p, "ERROR: "); + if (!p) + { + break; + } + p += 8; /* skip "ERROR: " */ + /* SQLSTATE: exactly 5 alphanumeric characters followed by ':' */ + if (strlen(p) >= 6 && p[5] == ':') + { + bool valid = true; + for (int i = 0; i < 5 && valid; i++) + { + valid = isalnum((unsigned char) p[i]); + } + if (valid) + { + int n = statelen < 6 ? statelen - 1 : 5; + memcpy(state, p, n); + state[n] = '\0'; + return; + } + } + } +} + + +static bool +exec_sql_on_service(TestRunner *r, const char *service, + const char *sql, char *outbuf, int outlen) +{ + char escaped[8192]; + escape_sql_for_shell(sql, escaped, sizeof(escaped)); + + /* + * Always run with ON_ERROR_STOP=1 so psql exits non-zero on SQL errors, + * and VERBOSITY=verbose so SQLSTATE codes appear in error output. + * Redirect stderr to stdout so both are captured. + * + * The monitor container only has the pg_auto_failover database (no + * "docker" default database), so supply explicit -U / -d for it. + */ + const char *connArgs = + (strcmp(service, "monitor") == 0) + ? "-U autoctl_node -d pg_auto_failover" + : ""; + + int rc = run_cmd_capture(outbuf, outlen, + "%s exec -T %s " + "psql --tuples-only --no-align " + "-v ON_ERROR_STOP=1 -v VERBOSITY=verbose " + "%s -c '%s' 2>&1", + r->composeBase, service, connArgs, escaped); + + return rc == 0; +} + + +/* ----------------------------------------------------------------------- + * Network failure simulation + * ----------------------------------------------------------------------- */ +static bool +runner_network_off(TestRunner *r, const char *nodeName) +{ + char netName[128], container[128]; + compose_network_name(r->projectName, netName, sizeof(netName)); + compose_container_name(r->projectName, nodeName, container, sizeof(container)); + + log_info(" Disconnecting %s from network %s", container, netName); + char out[1024] = ""; + int rc = run_cmd_capture(out, sizeof(out), + "docker network disconnect %s %s 2>&1", + netName, container); + if (rc != 0 && out[0]) + { + log_output(" ", out); + } + return rc == 0; +} + + +static bool +runner_network_on(TestRunner *r, const char *nodeName) +{ + char netName[128], container[128]; + compose_network_name(r->projectName, netName, sizeof(netName)); + compose_container_name(r->projectName, nodeName, container, sizeof(container)); + + log_info(" Reconnecting %s to network %s", container, netName); + char out[1024] = ""; + int rc = run_cmd_capture(out, sizeof(out), + "docker network connect %s %s 2>&1", + netName, container); + if (rc != 0 && out[0]) + { + log_output(" ", out); + } + return rc == 0; +} + + +/* ----------------------------------------------------------------------- + * Execute a single command + * ----------------------------------------------------------------------- */ + +/* + * runner_expand_macros substitutes %CIDR% in the args string with the + * Docker network CIDR reported by `pg_autoctl inspect show cidr` on the + * monitor container. The result is written to dst (at most dstlen bytes). + * Returns false if the CIDR could not be determined. + */ +static bool +runner_expand_macros(TestRunner *r, const char *args, char *dst, int dstlen, + char *errBuf, int errLen) +{ + const char *macro = "%CIDR%"; + const char *found = strstr(args, macro); + + if (!found) + { + strlcpy(dst, args, dstlen); + return true; + } + + /* fetch CIDR from the monitor */ + char cidr[128] = ""; + int rc = run_cmd_capture(cidr, sizeof(cidr), + "%s exec -T monitor pg_autoctl inspect show cidr 2>/dev/null", + r->composeBase); + if (rc != 0 || cidr[0] == '\0') + { + snprintf(errBuf, errLen, + "%%CIDR%%: could not get Docker network CIDR from monitor"); + return false; + } + + /* trim trailing newline */ + int n = strlen(cidr); + while (n > 0 && (cidr[n - 1] == '\n' || cidr[n - 1] == '\r' || cidr[n - 1] == ' ')) + { + cidr[--n] = '\0'; + } + + log_info(" %%CIDR%% = %s", cidr); + + /* substitute all occurrences */ + int di = 0; + for (const char *p = args; *p && di < dstlen - 1;) + { + if (strncmp(p, macro, strlen(macro)) == 0) + { + int cl = strlen(cidr); + if (di + cl < dstlen - 1) + { + memcpy(dst + di, cidr, cl); + di += cl; + } + p += strlen(macro); + } + else + { + dst[di++] = *p++; + } + } + dst[di] = '\0'; + return true; +} + + +static bool +runner_exec_cmd(TestRunner *r, TestCmd *cmd, char *errBuf, int errLen) +{ + switch (cmd->kind) + { + case CMD_EXEC: + { + char expandedArgs[4096] = ""; + if (!runner_expand_macros(r, cmd->args, expandedArgs, + sizeof(expandedArgs), errBuf, errLen)) + { + return false; + } + + char out[4096] = ""; + int rc = run_cmd_capture(out, sizeof(out), + "%s exec -T %s %s 2>&1", + r->composeBase, + cmd->service, expandedArgs); + + /* store output so a following expect{} can match it */ + strlcpy(r->lastSqlOutput, out, sizeof(r->lastSqlOutput)); + r->lastSqlFailed = false; + if (rc != 0) + { + if (out[0]) + { + log_output(" ", out); + } + snprintf(errBuf, errLen, + "exec %s %s failed (exit %d)", + cmd->service, expandedArgs, rc); + return false; + } + + /* + * Several pg_autoctl commands are synchronous from the monitor's + * perspective but leave NOTIFY messages buffered in the libpq + * socket. Drain them with proper '*' marks and confirm the + * expected post-command state before moving to the next step. + * + * perform failover / perform switchover / perform promotion: + * The entire FSM cycle completes inside the blocking exec. + * Wait for the formation to settle at primary+secondary. + * + * enable maintenance: + * The node transitions to maintenance asynchronously after + * the command returns. Wait for cmd->service to reach + * "maintenance" (single-node LISTEN-driven wait). + */ + + /* + * Only trigger implicit waits for direct pg_autoctl invocations, + * not for bash -c "..." wrappers where the match is incidental. + */ + bool is_pg_autoctl = (strncmp(expandedArgs, "pg_autoctl", 10) == 0); + if (is_pg_autoctl && + (strstr(expandedArgs, "perform failover") != NULL || + strstr(expandedArgs, "perform switchover") != NULL || + strstr(expandedArgs, "perform promotion") != NULL || + strstr(expandedArgs, "disable maintenance") != NULL)) + { + static const char fsStates[2][64] = { "primary", "secondary" }; + if (!monitor_wait_formation_states(r, fsStates, 2, + NULL, 0, 120)) + { + snprintf(errBuf, errLen, + "exec %s %s: timed out waiting for " + "primary+secondary", + cmd->service, expandedArgs); + return false; + } + } + else if (is_pg_autoctl && strstr(expandedArgs, "enable maintenance") != NULL) + { + /* + * Drain any notifications already buffered from the enable + * maintenance command before calling wait_for_state, so the + * maintenance convergence notification gets the '*' mark here + * rather than spilling unmarked into the next inter-command drain. + */ + if (r->notifyConnected) + { + const char *mn = cmd->service, *ms = "maintenance"; + bool sat = false; + runner_drain_notify(r, NULL, &mn, &ms, 1, &sat); + } + if (!wait_for_state(r, cmd->service, "maintenance", 60, false)) + { + snprintf(errBuf, errLen, + "exec %s %s: timed out waiting for maintenance", + cmd->service, expandedArgs); + return false; + } + } + return true; + } + + case CMD_EXEC_FAILS: + { + char out[4096] = ""; + int rc = run_cmd_capture(out, sizeof(out), + "%s exec -T %s %s 2>&1", + r->composeBase, + cmd->service, cmd->args); + if (rc == 0) + { + if (out[0]) + { + log_output(" ", out); + } + snprintf(errBuf, errLen, + "exec-fails %s %s: command succeeded (exit 0) " + "but expected failure", + cmd->service, cmd->args); + return false; + } + log_debug("exec-fails %s %s: exited with %d (expected)", + cmd->service, cmd->args, rc); + return true; + } + + case CMD_WAIT_STATE: + { + /* + * LISTEN-driven wait (items 5 & 6). + * + * runner_wait_notify_goal() watches the "state" channel and fires + * when goalState == target for this node. Pass-through states are + * also tracked from goalState notifications (not by polling). + * After success, verify all pass-through states were observed. + */ + bool seenThrough[PGAF_MAX_WAIT_STATES] = { false }; + + if (!runner_wait_notify_goal(r, cmd->service, cmd->state, + (const char (*)[64])cmd->passThroughStates, + seenThrough, cmd->passThroughCount, + cmd->timeoutSeconds)) + { + snprintf(errBuf, errLen, + "timeout: %s assigned-state never reached %s", + cmd->service, cmd->state); + return false; + } + + for (int i = 0; i < cmd->passThroughCount; i++) + { + if (!seenThrough[i]) + { + snprintf(errBuf, errLen, + "%s did not pass through assigned-state %s " + "on way to %s", + cmd->service, + cmd->passThroughStates[i], + cmd->state); + return false; + } + } + return true; + } + + case CMD_WAIT_STATES: + { + if (!wait_for_states(r, cmd)) + { + /* wait_for_states logs its own error detail */ + char label[128] = ""; + for (int i = 0; i < cmd->waitStateCount; i++) + { + if (i > 0) + { + strncat(label, ",", sizeof(label) - strlen(label) - 1); + } + strncat(label, cmd->waitStates[i], + sizeof(label) - strlen(label) - 1); + } + snprintf(errBuf, errLen, + "timeout: formation never reached states {%s}", label); + return false; + } + return true; + } + + case CMD_PROMOTE: + { + for (int i = 0; i < cmd->promoteCount; i++) + { + if (!runner_promote_one(r, cmd->promoteNodes[i])) + { + snprintf(errBuf, errLen, + "promote: could not make %s primary", + cmd->promoteNodes[i]); + return false; + } + } + return true; + } + + case CMD_ASSERT_STATE: + case CMD_ASSERT_ASSIGNED: + { + /* when a timeout is set this is a "wait until" — poll until match */ + if (cmd->timeoutSeconds > 0) + { + if (!wait_for_state(r, cmd->service, cmd->state, + cmd->timeoutSeconds, + cmd->kind == CMD_ASSERT_ASSIGNED)) + { + snprintf(errBuf, errLen, + "timeout: %s %s never reached %s", + cmd->service, + cmd->kind == CMD_ASSERT_ASSIGNED + ? "assigned-state" : "state", + cmd->state); + return false; + } + return true; + } + + char reported[64] = "", assigned[64] = ""; + if (!monitor_get_node_state(r, cmd->service, + reported, sizeof(reported), + assigned, sizeof(assigned))) + { + snprintf(errBuf, errLen, + "cannot reach monitor to assert %s state", + cmd->service); + return false; + } + const char *actual = (cmd->kind == CMD_ASSERT_ASSIGNED) + ? assigned : reported; + if (strcmp(actual, cmd->state) != 0) + { + snprintf(errBuf, errLen, + "%s %s is \"%s\", expected \"%s\"", + cmd->service, + (cmd->kind == CMD_ASSERT_ASSIGNED) + ? "assigned-state" : "state", + actual, cmd->state); + return false; + } + return true; + } + + case CMD_SQL: + { + strlcpy(r->lastSqlService, cmd->service, sizeof(r->lastSqlService)); + r->lastSqlFailed = false; + r->lastSqlState[0] = '\0'; + r->lastSqlOutput[0] = '\0'; + + if (!exec_sql_on_service(r, cmd->service, cmd->args, + r->lastSqlOutput, + sizeof(r->lastSqlOutput))) + { + if (cmd->allowError) + { + /* + * Soft failure: expected by the following CMD_EXPECT_ERROR. + * Record the SQLSTATE and clear the output (it's an error + * message, not usable result rows). + */ + r->lastSqlFailed = true; + parse_sqlstate(r->lastSqlOutput, r->lastSqlState, + sizeof(r->lastSqlState)); + log_debug("sql on %s failed with SQLSTATE %s (expected)", + cmd->service, + r->lastSqlState[0] ? r->lastSqlState : "(unknown)"); + r->lastSqlOutput[0] = '\0'; + return true; + } + snprintf(errBuf, errLen, + "sql on %s failed:\n%s", cmd->service, r->lastSqlOutput); + return false; + } + log_debug("sql output: %s", r->lastSqlOutput); + return true; + } + + case CMD_EXPECT: + { + /* + * Substring match: the expect value is a pattern that must appear + * somewhere in the SQL output. This lets tests avoid depending on + * non-deterministic details like node IDs while still being specific + * enough to catch regressions. + */ + if (strstr(r->lastSqlOutput, cmd->expected) == NULL) + { + snprintf(errBuf, errLen, + "expected \"%s\", got \"%s\"", + cmd->expected, r->lastSqlOutput); + return false; + } + return true; + } + + case CMD_EXPECT_ERROR: + { + if (!r->lastSqlFailed) + { + snprintf(errBuf, errLen, + "expected SQL error on %s, but the query succeeded", + r->lastSqlService); + return false; + } + if (cmd->state[0] && + strcmp(r->lastSqlState, cmd->state) != 0) + { + snprintf(errBuf, errLen, + "expected SQLSTATE %s on %s, got %s", + cmd->state, r->lastSqlService, + r->lastSqlState[0] ? r->lastSqlState : "(unknown)"); + return false; + } + r->lastSqlFailed = false; /* consumed */ + return true; + } + + case CMD_NETWORK_OFF: + { + if (!runner_network_off(r, cmd->service)) + { + snprintf(errBuf, errLen, + "network disconnect %s failed", cmd->service); + return false; + } + return true; + } + + case CMD_NETWORK_ON: + { + if (!runner_network_on(r, cmd->service)) + { + snprintf(errBuf, errLen, + "network connect %s failed", cmd->service); + return false; + } + return true; + } + + case CMD_SLEEP: + { + sleep(cmd->timeoutSeconds); + return true; + } + + case CMD_COMPOSE_DOWN: + { + return runner_compose_down(r); + } + + case CMD_COMPOSE_START: + { + /* + * Use `up -d --no-recreate --no-deps` rather than `start`: + * `start` only works for already-created containers, but deferred + * nodes (launch deferred) are never created during compose up, + * so `start` would fail for them. `up -d --no-recreate --no-deps` + * handles both: creates+starts a new container, or starts an + * existing stopped one without touching other services. + * + * `--no-deps` is critical: without it, `start` (and `up`) would + * also start any stopped services listed in `depends_on`, which + * interferes with tests that restart a single node. + */ + char out[4096] = ""; + int rc = run_cmd_capture(out, sizeof(out), + "%s up -d --no-recreate --no-deps %s 2>&1", + r->composeBase, cmd->service); + if (out[0]) + { + log_output(" compose: ", out); + } + if (rc != 0) + { + snprintf(errBuf, errLen, + "docker compose start %s failed (exit %d)", + cmd->service, rc); + return false; + } + return true; + } + + case CMD_COMPOSE_STOP: + { + char out[4096] = ""; + int rc = run_cmd_capture(out, sizeof(out), + "%s stop %s 2>&1", + r->composeBase, cmd->service); + if (out[0]) + { + log_output(" compose: ", out); + } + if (rc != 0) + { + snprintf(errBuf, errLen, + "docker compose stop %s failed (exit %d)", + cmd->service, rc); + return false; + } + return true; + } + + case CMD_COMPOSE_KILL: + { + char out[4096] = ""; + int rc = run_cmd_capture(out, sizeof(out), + "%s kill %s 2>&1", + r->composeBase, cmd->service); + if (out[0]) + { + log_output(" compose: ", out); + } + if (rc != 0) + { + snprintf(errBuf, errLen, + "docker compose kill %s failed (exit %d)", + cmd->service, rc); + return false; + } + return true; + } + + /* + * CMD_COMPOSE_INJECT: copy a file from a Docker image into a running + * service container without restarting it. + * + * compose inject pgaf:next /usr/local/bin/pg_autoctl monitor:/usr/local/bin/pg_autoctl + * + * Fields: expected=image args=src-path service=dst-svc state=dst-path + * + * Sequence: + * 1. docker create --name _pgaf_inject_tmp + * 2. docker cp _pgaf_inject_tmp: /tmp/_pgaf_inject_binary + * 3. docker rm _pgaf_inject_tmp + * 4. docker cp /tmp/_pgaf_inject_binary : + */ + case CMD_COMPOSE_INJECT: + { + char out[4096] = ""; + int rc; + + log_info("compose inject: creating temporary container from %s", + cmd->expected); + + rc = run_cmd_capture(out, sizeof(out), + "docker create --name _pgaf_inject_tmp %s 2>&1", + cmd->expected); + if (out[0]) + { + log_output(" inject: ", out); + } + if (rc != 0) + { + snprintf(errBuf, errLen, + "compose inject: docker create %s failed (exit %d)", + cmd->expected, rc); + return false; + } + + out[0] = '\0'; + rc = run_cmd_capture(out, sizeof(out), + "docker cp _pgaf_inject_tmp:%s /tmp/_pgaf_inject_binary 2>&1", + cmd->args); + if (out[0]) + { + log_output(" inject: ", out); + } + + /* always remove the temp container, even on failure */ + run_cmd_capture(out, sizeof(out), + "docker rm _pgaf_inject_tmp 2>&1"); + if (rc != 0) + { + snprintf(errBuf, errLen, + "compose inject: docker cp from image failed (exit %d)", rc); + return false; + } + + char container[256]; + compose_container_name(r->projectName, cmd->service, + container, sizeof(container)); + + out[0] = '\0'; + rc = run_cmd_capture(out, sizeof(out), + "docker cp /tmp/_pgaf_inject_binary %s:%s 2>&1", + container, cmd->state); + if (out[0]) + { + log_output(" inject: ", out); + } + if (rc != 0) + { + snprintf(errBuf, errLen, + "compose inject: docker cp to %s:%s failed (exit %d)", + cmd->service, cmd->state, rc); + return false; + } + + log_info("compose inject: %s:%s → %s:%s", + cmd->expected, cmd->args, cmd->service, cmd->state); + return true; + } + + /* + * CMD_WAIT_MULTI: wait until all (node, state) pairs are satisfied. + * + * wait until node2 state is primary and node1 state is secondary + * with timeout 90s + * + * Polls each condition in turn; repeats until all are true or timeout. + */ + case CMD_WAIT_MULTI: + { + int timeoutSecs = cmd->timeoutSeconds; + time_t deadline = time(NULL) + timeoutSecs; + + /* build pointer arrays once — used for marking on every drain */ + const char *mn[PGAF_MAX_WAIT_STATES], *ms[PGAF_MAX_WAIT_STATES]; + for (int i = 0; i < cmd->waitStateCount; i++) + { + mn[i] = cmd->waitNodes[i]; + ms[i] = cmd->waitStates[i]; + } + + /* LISTEN-based satisfied[]: set by runner_drain_notify when a + * convergence notification arrives for a specific (node, state) pair */ + bool listenSatisfied[PGAF_MAX_WAIT_STATES] = { false }; + + while (time(NULL) < deadline) + { + /* + * Try subprocess-based check first. monitor_get_node_state runs + * "pg_autoctl inspect monitor node-state" which requires v2.2. + * When nodes run v2.1 the call returns false; track how many + * succeed so we can fall back to LISTEN-based convergence. + */ + bool allMet = true; + int subproc_ok = 0; + for (int i = 0; i < cmd->waitStateCount; i++) + { + char reported[64] = "", assigned[64] = ""; + if (!monitor_get_node_state(r, cmd->waitNodes[i], + reported, sizeof(reported), + assigned, sizeof(assigned))) + { + allMet = false; /* can't confirm via subprocess */ + /* don't break — try to drain anyway */ + continue; + } + subproc_ok++; + + /* + * Require convergence: the node must have reported the + * target state (reportedState) and the monitor must agree + * it belongs there (assignedState / goalState). Checking + * only reportedState could catch a transient pass-through; + * checking only assignedState fires before the node confirms. + */ + if (strcmp(reported, cmd->waitStates[i]) != 0 || + strcmp(assigned, cmd->waitStates[i]) != 0) + { + allMet = false; + } + } + + /* + * Drain: log notifications, mark '*' on convergence events, and + * set listenSatisfied[i] when the (node, state) pair converges. + */ + runner_drain_notify(r, NULL, mn, ms, + cmd->waitStateCount, listenSatisfied); + + /* + * When subprocess is unavailable (v2.1 nodes), fall back to + * LISTEN-based convergence. If all conditions have been seen + * converged via NOTIFY, treat that as success. + */ + if (subproc_ok == 0) + { + bool allListenMet = true; + for (int i = 0; i < cmd->waitStateCount; i++) + { + if (!listenSatisfied[i]) + { + allListenMet = false; + break; + } + } + if (allListenMet) + { + return true; + } + } + else if (allMet) + { + return true; + } + + runner_wait_socket(r, 1000); + } + + /* build readable label for error */ + char label[512] = ""; + for (int i = 0; i < cmd->waitStateCount; i++) + { + if (i > 0) + { + strncat(label, " and ", sizeof(label) - strlen(label) - 1); + } + char part[128]; + snprintf(part, sizeof(part), "%s=%s", + cmd->waitNodes[i], cmd->waitStates[i]); + strncat(label, part, sizeof(label) - strlen(label) - 1); + } + snprintf(errBuf, errLen, + "timeout: conditions not met: %s", label); + return false; + } + + /* + * CMD_PG_AUTOCTL: run pg_autoctl locally in the pgaftest process. + * + * When running as a compose service, PG_AUTOCTL_MONITOR is set in the + * container environment (see compose_gen.c) so pg_autoctl finds the + * monitor URI automatically without any --monitor injection here. + * + * When running on the host (pgaftest run outside Docker), the variable + * must be set by the user or in their shell environment. + */ + case CMD_PG_AUTOCTL: + { + char out[4096] = ""; + int rc = run_cmd_capture(out, sizeof(out), + "pg_autoctl %s 2>&1", cmd->args); + if (rc != 0) + { + if (out[0]) + { + log_output(" ", out); + } + snprintf(errBuf, errLen, + "pg_autoctl %s failed (exit %d)", cmd->args, rc); + return false; + } + return true; + } + + case CMD_WAIT_STOPPED: + { + const char *svc = cmd->service; + int timeoutSecs = cmd->timeoutSeconds; + time_t deadline = time(NULL) + timeoutSecs; + + log_info(" Waiting for service %s to stop (timeout %ds)", + svc, timeoutSecs); + + while (time(NULL) < deadline) + { + /* + * `docker compose ps --format {{.State}} ` outputs the + * container state: "running", "exited", "paused", etc. + * An empty or non-running result also counts as stopped. + */ + char state[64] = ""; + int rc = run_cmd_capture(state, sizeof(state), + "%s ps --format '{{.State}}' %s" + " 2>/dev/null", + r->composeBase, svc); + + /* strip whitespace */ + int n = strlen(state); + while (n > 0 && (state[n - 1] == '\n' || state[n - 1] == ' ')) + { + state[--n] = '\0'; + } + + if (rc != 0 || state[0] == '\0' || + strcmp(state, "running") != 0) + { + log_info(" Service %s stopped (state: %s)", svc, + state[0] ? state : "gone"); + return true; + } + + usleep(500000); /* 500ms */ + } + + snprintf(errBuf, errLen, + "timeout: service %s did not stop within %ds", + svc, timeoutSecs); + return false; + } + + case CMD_STOP_POSTGRES: + { + /* + * Signal the pg_autoctl postgres-controller service to stop + * Postgres. Equivalent to calling `pg_autoctl manual service pgctl off`. + */ + log_info("Stopping Postgres on %s", cmd->service); + int rc = run_cmd( + "%s exec %s pg_autoctl manual service pgctl off" + " --pgdata /var/lib/postgres/pgaf", + r->composeBase, cmd->service); + if (rc != 0) + { + snprintf(errBuf, errLen, + "stop postgres %s failed (exit %d)", + cmd->service, rc); + return false; + } + return true; + } + + case CMD_START_POSTGRES: + { + log_info("Starting Postgres on %s", cmd->service); + int rc = run_cmd( + "%s exec %s pg_autoctl manual service pgctl on" + " --pgdata /var/lib/postgres/pgaf", + r->composeBase, cmd->service); + if (rc != 0) + { + snprintf(errBuf, errLen, + "start postgres %s failed (exit %d)", + cmd->service, rc); + return false; + } + return true; + } + + case CMD_STAYS_WHILE: + { + /* + * LISTEN-driven stays-while (item 9). + * + * Strategy: + * 1. Connect to LISTEN "state" channel. + * 2. Verify the node's current assigned-state matches expectation. + * 3. Flush any pending notifications (establish clean baseline). + * 4. Run each body command. + * 5. After each command, drain the notification queue and fail + * immediately if any notification for this node has goalState + * != expectedState. + * 6. Final SQL check after the body. + * + * The LISTEN approach catches state-change assignments that happen + * *during* a body command, not just between commands. + */ + log_info("assert %s stays %s while running body", + cmd->service, cmd->state); + + runner_notify_connect(r); + + /* initial assigned-state check */ + char curReported[64] = "", curAssigned[64] = ""; + if (!monitor_get_node_state(r, cmd->service, + curReported, sizeof(curReported), + curAssigned, sizeof(curAssigned))) + { + snprintf(errBuf, errLen, + "stays-while: could not get state of %s", cmd->service); + return false; + } + if (strcmp(curAssigned, cmd->state) != 0) + { + snprintf(errBuf, errLen, + "stays-while: %s assigned-state is %s, " + "expected %s (before body)", + cmd->service, curAssigned, cmd->state); + return false; + } + + /* flush any pre-existing notifications so we start from a clean slate */ + if (r->notifyConnected) + { + runner_drain_notify(r, NULL, NULL, NULL, 0, NULL); + } + + /* run each body command, checking notifications after each */ + for (TestCmd *bc = cmd->body; bc; bc = bc->next) + { + if (!runner_exec_cmd(r, bc, errBuf, errLen)) + { + return false; + } + + /* check notifications: any goalState change for this node? */ + if (!runner_check_stays_notify(r, cmd->service, cmd->state, + errBuf, errLen)) + { + return false; + } + + /* also SQL-poll when LISTEN is unavailable */ + if (!r->notifyConnected) + { + if (!monitor_get_node_state(r, cmd->service, + curReported, sizeof(curReported), + curAssigned, sizeof(curAssigned))) + { + snprintf(errBuf, errLen, + "stays-while: could not get state of %s after command", + cmd->service); + return false; + } + if (strcmp(curAssigned, cmd->state) != 0) + { + snprintf(errBuf, errLen, + "stays-while: %s assigned-state changed to %s, " + "expected %s during body", + cmd->service, curAssigned, cmd->state); + return false; + } + } + } + + /* final check after all body commands */ + if (!runner_check_stays_notify(r, cmd->service, cmd->state, + errBuf, errLen)) + { + return false; + } + if (!r->notifyConnected) + { + if (!monitor_get_node_state(r, cmd->service, + curReported, sizeof(curReported), + curAssigned, sizeof(curAssigned)) || + strcmp(curAssigned, cmd->state) != 0) + { + snprintf(errBuf, errLen, + "stays-while: %s assigned-state is %s " + "after body, expected %s", + cmd->service, curAssigned, cmd->state); + return false; + } + } + return true; + } + + case CMD_SET_MONITOR: + { + /* + * Switch the active monitor service: drop the existing LISTEN + * connection (if any) and reconnect to the new monitor. All + * subsequent LISTEN/NOTIFY waits and monitor_get_node_state() + * calls will target the new service. + */ + log_info(" set monitor: switching to service \"%s\"", + cmd->service); + + if (r->notifyConnected) + { + pgsql_finish(&r->notifyConn); + r->notifyConnected = false; + } + + strlcpy(r->activeMonitorService, cmd->service, + sizeof(r->activeMonitorService)); + + /* reconnect immediately so wait loops can start right away */ + runner_notify_connect(r); + return true; + } + + case CMD_LOGS_CHECK: + { + /* + * Grep the container logs. + * cmd->allowError == false → fixed-string grep (-F) + * cmd->allowError == true → PCRE grep (-P) + * cmd->logsNegate → assert pattern NOT found + */ + bool pcre = cmd->allowError; + bool negate = cmd->logsNegate; + const char *flag = pcre ? "-qP" : "-qF"; + + /* Shell-quote the pattern by wrapping in single quotes. + * Replace any embedded ' with '\'' to safely handle patterns + * such as PCRE lookahead strings. + */ + char quotedPat[sizeof(cmd->args) * 4 + 4]; + { + char *q = quotedPat; + *q++ = '\''; + for (const char *p = cmd->args; *p; p++) + { + if (*p == '\'') + { + *q++ = '\''; + *q++ = '\\'; + *q++ = '\''; + *q++ = '\''; + } + else + { + *q++ = *p; + } + } + *q++ = '\''; + *q = '\0'; + } + + char grepCmd[8192]; + snprintf(grepCmd, sizeof(grepCmd), + "docker compose --project-directory %s logs --no-color %s 2>&1 | grep %s %s", + r->workDir, + cmd->service, + flag, + quotedPat); + + log_info(" logs %s: %s%s \"%s\"", + cmd->service, + negate ? "not " : "", + pcre ? "matches" : "contains", + cmd->args); + + int rc = system(grepCmd); + bool found = (rc == 0); + + if (negate && found) + { + log_error("logs check failed: pattern found in %s logs: %s", + cmd->service, cmd->args); + return false; + } + else if (!negate && !found) + { + log_error("logs check failed: pattern not found in %s logs: %s", + cmd->service, cmd->args); + return false; + } + return true; + } + } + return true; +} + + +/* ----------------------------------------------------------------------- + * Execute a step (list of commands) + * ----------------------------------------------------------------------- */ + +/* + * log_output prints each non-empty line of a captured command output block + * at INFO level, prefixed with `prefix`. Used to fold multi-line output + * from docker compose / docker network into the structured log stream. + */ +static void +log_output(const char *prefix, const char *out) +{ + char buf[4096]; + strlcpy(buf, out, sizeof(buf)); + for (char *line = buf, *nl; line && *line; line = nl ? nl + 1 : NULL) + { + nl = strchr(line, '\n'); + if (nl) + { + *nl = '\0'; + } + + /* skip blank / pure-whitespace lines */ + const char *p = line; + while (*p == ' ' || *p == '\t' || *p == '\r') + { + p++; + } + if (*p) + { + log_info("%s%s", prefix, line); + } + } +} + + +/* + * inline_text copies src into dst, collapsing runs of whitespace (including + * newlines) into a single space and trimming leading/trailing whitespace. + * Result is always NUL-terminated and fits in dstlen bytes. + */ +static void +inline_text(const char *src, char *dst, int dstlen) +{ + int di = 0; + bool space = true; /* suppress leading whitespace */ + for (const char *p = src; *p && di < dstlen - 1; p++) + { + if (*p == '\n' || *p == '\r' || *p == '\t' || *p == ' ') + { + if (!space && di < dstlen - 1) + { + dst[di++] = ' '; + } + space = true; + } + else + { + dst[di++] = *p; + space = false; + } + } + + /* trim trailing space */ + while (di > 0 && dst[di - 1] == ' ') + { + di--; + } + dst[di] = '\0'; +} + + +/* + * cmd_label returns a human-readable description of a command matching the + * DSL syntax from the spec file. The caller owns the buffer. + */ +static void +cmd_label(const TestCmd *cmd, char *buf, int len) +{ + char tmp[512]; + + switch (cmd->kind) + { + case CMD_EXEC: + { + snprintf(buf, len, "exec %s %s", cmd->service, cmd->args); + break; + } + + case CMD_EXEC_FAILS: + { + snprintf(buf, len, "exec-fails %s %s", cmd->service, cmd->args); + break; + } + + case CMD_WAIT_STATE: + { + snprintf(buf, len, "wait until %s state = %s timeout %ds", + cmd->service, cmd->state, cmd->timeoutSeconds); + break; + } + + case CMD_WAIT_STATES: + { + char states[256] = ""; + for (int i = 0; i < cmd->waitStateCount; i++) + { + if (i > 0) + { + strncat(states, ", ", sizeof(states) - strlen(states) - 1); + } + strncat(states, cmd->waitStates[i], sizeof(states) - strlen(states) - 1); + } + snprintf(buf, len, "wait until %s timeout %ds", states, + cmd->timeoutSeconds); + break; + } + + case CMD_ASSERT_STATE: + { + snprintf(buf, len, "assert %s state = %s", cmd->service, cmd->state); + break; + } + + case CMD_ASSERT_ASSIGNED: + { + snprintf(buf, len, "assert %s assigned-state = %s", + cmd->service, cmd->state); + break; + } + + case CMD_PROMOTE: + { + char nodes[256] = ""; + for (int i = 0; i < cmd->promoteCount; i++) + { + if (i > 0) + { + strncat(nodes, ", ", sizeof(nodes) - strlen(nodes) - 1); + } + strncat(nodes, cmd->promoteNodes[i], + sizeof(nodes) - strlen(nodes) - 1); + } + snprintf(buf, len, "promote %s", nodes[0] ? nodes : cmd->service); + break; + } + + case CMD_SQL: + { + inline_text(cmd->args, tmp, sizeof(tmp)); + snprintf(buf, len, "sql %s { %s }", cmd->service, tmp); + break; + } + + case CMD_EXPECT: + { + if (strchr(cmd->expected, '\n')) + { + /* multi-row: reconstruct { row } { row } display form */ + char rows[256] = { 0 }; + int rpos = 0; + const char *p = cmd->expected; + while (*p) + { + const char *nl = strchr(p, '\n'); + int rowlen = nl ? (int) (nl - p) : (int) strlen(p); + int n = snprintf(rows + rpos, sizeof(rows) - rpos, + "%s{ %.*s }", + rpos ? " " : "", rowlen, p); + if (n > 0) + { + rpos += n; + } + p = nl ? nl + 1 : p + rowlen; + } + snprintf(buf, len, "expect { %s }", rows); + } + else + { + inline_text(cmd->expected, tmp, sizeof(tmp)); + snprintf(buf, len, "expect { %s }", tmp); + } + break; + } + + case CMD_EXPECT_ERROR: + { + snprintf(buf, len, "expect error %s", cmd->state); + break; + } + + case CMD_NETWORK_OFF: + { + snprintf(buf, len, "network disconnect %s", cmd->service); + break; + } + + case CMD_NETWORK_ON: + { + snprintf(buf, len, "network connect %s", cmd->service); + break; + } + + case CMD_SLEEP: + { + snprintf(buf, len, "sleep %ds", cmd->timeoutSeconds); + break; + } + + case CMD_COMPOSE_DOWN: + { + snprintf(buf, len, "compose down"); + break; + } + + case CMD_COMPOSE_START: + { + snprintf(buf, len, "compose start %s", cmd->service); + break; + } + + case CMD_COMPOSE_STOP: + { + snprintf(buf, len, "compose stop %s", cmd->service); + break; + } + + case CMD_COMPOSE_KILL: + { + snprintf(buf, len, "compose kill %s", cmd->service); + break; + } + + case CMD_COMPOSE_INJECT: + { + snprintf(buf, len, "compose inject %s %s %s:%s", + cmd->expected, cmd->args, cmd->service, cmd->state); + break; + } + + case CMD_WAIT_STOPPED: + { + snprintf(buf, len, "wait until %s stopped timeout %ds", + cmd->service, cmd->timeoutSeconds); + break; + } + + case CMD_WAIT_MULTI: + { + char parts[512] = ""; + for (int i = 0; i < cmd->waitStateCount; i++) + { + if (i > 0) + { + strncat(parts, " and ", sizeof(parts) - strlen(parts) - 1); + } + char piece[128]; + snprintf(piece, sizeof(piece), "%s state = %s", + cmd->waitNodes[i], cmd->waitStates[i]); + strncat(parts, piece, sizeof(parts) - strlen(parts) - 1); + } + snprintf(buf, len, "wait until %s timeout %ds", parts, + cmd->timeoutSeconds); + break; + } + + case CMD_PG_AUTOCTL: + { + snprintf(buf, len, "pg_autoctl %s", cmd->args); + break; + } + + case CMD_STOP_POSTGRES: + { + snprintf(buf, len, "stop postgres %s", cmd->service); + break; + } + + case CMD_START_POSTGRES: + { + snprintf(buf, len, "start postgres %s", cmd->service); + break; + } + + case CMD_STAYS_WHILE: + { + snprintf(buf, len, "assert %s stays %s while { ... }", + cmd->service, cmd->state); + break; + } + + case CMD_SET_MONITOR: + { + snprintf(buf, len, "set monitor %s", cmd->service); + break; + } + + case CMD_LOGS_CHECK: + { + snprintf(buf, len, "logs %s %s%s \"%s\"", + cmd->service, + cmd->logsNegate ? "not " : "", + cmd->allowError ? "matches" : "contains", + cmd->args); + break; + } + + default: + { + snprintf(buf, len, "(unknown command %d)", cmd->kind); + break; + } + } +} + + +static bool +runner_exec_step(TestRunner *r, TestStep *step, char *errBuf, int errLen, + int stepNum) +{ + int cmdNum = 0; + for (TestCmd *cmd = step->commands; cmd; cmd = cmd->next) + { + /* + * Flush all notifications that arrived during the previous command. + * Loop until the socket is idle for 50 ms so we catch notifications + * still in-flight in the TCP stream, not just what libpq buffered. + */ + if (r->notifyConnected) + { + while (runner_wait_socket(r, 50)) + { + runner_drain_notify(r, NULL, NULL, NULL, 0, NULL); + } + runner_drain_notify(r, NULL, NULL, NULL, 0, NULL); /* one last sweep of the libpq buffer */ + } + + char label[256]; + cmd_label(cmd, label, sizeof(label)); + if (stepNum > 0) + { + log_info(" [%02d-%02d] %s", stepNum, ++cmdNum, label); + } + else + { + log_info(" [%02d] %s", ++cmdNum, label); + } + if (!runner_exec_cmd(r, cmd, errBuf, errLen)) + { + return false; + } + } + return true; +} + + +/* ----------------------------------------------------------------------- + * Load runner state from workDir (for step/down subcommands) + * ----------------------------------------------------------------------- */ +static bool +runner_load_state(TestRunner *r) +{ + /* Check the compose file exists — if it does, the stack should be up */ + struct stat st; + if (stat(r->composeFile, &st) != 0) + { + log_error("No compose file found at \"%s\"; run `pgaftest setup` first", + r->composeFile); + return false; + } + r->composeUp = true; + return true; +} + + +/* ----------------------------------------------------------------------- + * Monitor readiness ping loop + * + * When running inside the compose network (PGAFTEST_COMPOSE_SERVICE=1) + * the pgaftest container starts at the same time as the monitor — no + * depends_on ordering. We spin here until the monitor postgres accepts + * connections, then open the LISTEN channel. + * ----------------------------------------------------------------------- */ + +#define MONITOR_WAIT_TIMEOUT_SECS 120 + +static bool +runner_wait_for_monitor(TestRunner *r) +{ + if (!r->spec->cluster.withMonitor) + { + return true; + } + + log_info("Waiting for monitor to become ready (timeout %ds)", + MONITOR_WAIT_TIMEOUT_SECS); + + time_t deadline = time(NULL) + MONITOR_WAIT_TIMEOUT_SECS; + + char monitorReadyCmd[sizeof(r->composeBase) + 128]; + snprintf(monitorReadyCmd, sizeof(monitorReadyCmd), + "%s exec -T monitor psql -U autoctl_node -d pg_auto_failover " + "-c 'SELECT 1' >/dev/null 2>&1", + r->composeBase); + + while (time(NULL) < deadline) + { + if (runner_notify_connect(r)) + { + log_info("Monitor is ready; LISTEN channel open"); + return true; + } + + /* + * If the direct libpq connection can't be established (e.g. the + * host IP is not in the monitor's pg_hba.conf — common when the + * monitor was initialised by an older pg_autoctl version that only + * added the Docker network CIDR), fall back to a subprocess check + * via docker compose exec. Once the monitor responds to psql we + * return true; wait loops will use subprocess polling instead of + * LISTEN/NOTIFY. + */ + if (run_cmd("%s", monitorReadyCmd) == 0) + { + log_info("Monitor is ready (subprocess check; LISTEN not available)"); + return true; + } + + pg_usleep(500 * 1000); /* 0.5 s between attempts */ + } + + log_error("Timed out waiting for monitor to become ready after %ds", + MONITOR_WAIT_TIMEOUT_SECS); + return false; +} + + +/* ----------------------------------------------------------------------- + * Public API + * ----------------------------------------------------------------------- */ +bool +runner_run(TestSpec *spec, const char *workDir, bool noCleanup) +{ + TestRunner r; + runner_init(&r, spec, workDir); + + if (!runner_compose_generate(&r)) + { + return false; + } + + /* + * HOST path: start the compose stack, then run tests directly from the host + * using docker-compose exec for all node/monitor interactions. + * + * The compose file does not include a pgaftest service in host mode — the + * current process IS the test runner and has docker access already. + * + * When PGAFTEST_COMPOSE_SERVICE is set the compose file was generated to + * include a pgaftest service and we're running inside that container; skip + * compose up since the stack is already running. + */ + if (!getenv("PGAFTEST_COMPOSE_SERVICE")) + { + log_info("Starting compose stack (project: %s)", r.projectName); + + /* + * Remove any leftover stack from a previous --no-cleanup run so we + * always start with clean volumes. + */ + run_cmd("%s down --volumes --remove-orphans 2>&1", r.composeBase); + + int up_rc = run_cmd("%s up --build -d 2>&1", r.composeBase); + if (up_rc != 0) + { + log_error("docker compose up failed (exit %d)", up_rc); + run_cmd("%s down --volumes 2>&1", r.composeBase); + return false; + } + } + + r.composeUp = true; + + /* + * If no sequence{} block was written, run steps in definition order. + * This lets simple test files omit the redundant sequence block entirely. + */ + if (spec->sequenceLength == 0) + { + for (TestStep *s = spec->steps; s; s = s->next) + { + if (spec->sequenceLength < PGAF_MAX_SEQ) + { + spec->sequence[spec->sequenceLength++] = s->name; + } + } + } + + r.tapTotal = spec->sequenceLength; + + if (!runner_wait_for_monitor(&r)) + { + return false; + } + + if (!runner_apply_formation_settings(&r)) + { + return false; + } + + /* setup{} */ + if (spec->setup) + { + char err[512] = ""; + log_info("Running setup block"); + if (!runner_exec_step(&r, spec->setup, err, sizeof(err), 0)) + { + tap_plan(&r); + tap_diag("setup failed: %s", err); + return false; + } + } + + /* sequence — fail fast: first failure stops the run */ + bool allPassed = true; + for (int i = 0; i < spec->sequenceLength; i++) + { + const char *name = spec->sequence[i]; + TestStep *step = spec_find_step(spec, name); + if (!step) + { + tap_not_ok(&r, name, "step not found in spec"); + allPassed = false; + break; + } + + log_info("STEP %d: %s", i + 1, name); + char err[512] = ""; + if (runner_exec_step(&r, step, err, sizeof(err), i + 1)) + { + tap_ok(&r, name); + } + else + { + tap_not_ok(&r, name, err); + allPassed = false; + break; + } + } + + tap_plan(&r); + + /* teardown{} — always runs */ + if (spec->teardown) + { + char err[512] = ""; + log_info("Running teardown block"); + runner_exec_step(&r, spec->teardown, err, sizeof(err), 0); + } + + /* compose lifecycle is owned by the host — do not call compose_down here */ + return allPassed; +} + + +bool +runner_setup(TestSpec *spec, const char *workDir, bool withTmux) +{ + TestRunner r; + runner_init(&r, spec, workDir); + + if (!runner_compose_generate(&r)) + { + return false; + } + + if (!runner_compose_up(&r)) + { + return false; + } + + if (!runner_wait_for_monitor(&r)) + { + runner_compose_down(&r); + return false; + } + + if (!runner_apply_formation_settings(&r)) + { + runner_compose_down(&r); + return false; + } + + /* run setup{} block */ + if (spec->setup) + { + char err[512] = ""; + log_info("Running setup block"); + if (!runner_exec_step(&r, spec->setup, err, sizeof(err), 0)) + { + log_error("Setup failed: %s", err); + runner_compose_down(&r); + return false; + } + } + + if (withTmux) + { + /* Launch tmux with compose logs + pg_autoctl watch */ + log_info("Starting tmux session for project %s", r.projectName); + run_cmd( + "tmux new-session -d -s %s " + "\"%s logs -f\" \\; " + "split-window -v " + "\"%s exec monitor pg_autoctl watch\" \\; " + "split-window -v \\; " + "select-layout even-vertical", + r.projectName, + r.composeBase, + r.composeBase); + + printf("Cluster ready. Attach with: tmux attach -t %s\n", + r.projectName); + printf("Tear down with: pgaftest down --work-dir %s\n", workDir); + } + else + { + printf("\nCluster ready — compose project: %s\n", r.projectName); + printf("Work dir: %s\n", workDir); + printf("\nAvailable steps:"); + for (TestStep *s = spec->steps; s; s = s->next) + { + printf(" %s", s->name); + } + printf("\n\nRun a step: pgaftest step --work-dir %s\n", workDir); + printf("Tear down: pgaftest down --work-dir %s\n\n", workDir); + } + + return true; +} + + +bool +runner_step(TestSpec *spec, const char *workDir, const char *stepName) +{ + TestRunner r; + runner_init(&r, spec, workDir); + + if (!runner_load_state(&r)) + { + return false; + } + + TestStep *step = spec_find_step(spec, stepName); + if (!step) + { + log_error("Step \"%s\" not found in spec", stepName); + return false; + } + + char err[512] = ""; + if (!runner_exec_step(&r, step, err, sizeof(err), 0)) + { + log_error("Step \"%s\" failed: %s", stepName, err); + return false; + } + + log_info("Step \"%s\" passed", stepName); + return true; +} + + +bool +runner_down(TestSpec *spec, const char *workDir) +{ + TestRunner r; + runner_init(&r, spec, workDir); + runner_load_state(&r); /* best-effort */ + + /* teardown{} */ + if (spec->teardown) + { + char err[512] = ""; + runner_exec_step(&r, spec->teardown, err, sizeof(err), 0); + } + + return runner_compose_down(&r); +} + + +bool +runner_show(TestSpec *spec) +{ + TestRunner r; + runner_init(&r, spec, "/tmp/pgaftest-show"); + + /* write to stdout instead of a file */ + /* No specFile for show — omit the pgaftest service */ + bool ok = compose_gen_write(&spec->cluster, "/dev/stdout", + r.projectName, r.contextDir, NULL); + return ok; +} + + +/* + * runner_prepare writes docker-compose.yml, per-node .ini files, and a + * Makefile into outDir, then prints the docker compose command to stdout. + * + * The Makefile has two phony targets: + * test: docker compose up --exit-code-from pgaftest + * down: docker compose down --volumes --remove-orphans + */ +bool +runner_prepare(TestSpec *spec, const char *outDir) +{ + TestRunner r; + + /* If outDir not given, derive from spec name next to spec file */ + char derivedDir[1024]; + if (!outDir || outDir[0] == '\0') + { + const char *base = strrchr(spec->filename, '/'); + base = base ? base + 1 : spec->filename; + + /* strip .pgaf extension for the directory name */ + char stem[256]; + strlcpy(stem, base, sizeof(stem)); + char *dot = strrchr(stem, '.'); + if (dot && strcmp(dot, ".pgaf") == 0) + { + *dot = '\0'; + } + + /* place it beside the spec file */ + const char *slash = strrchr(spec->filename, '/'); + if (slash) + { + snprintf(derivedDir, sizeof(derivedDir), "%.*s/%s-compose", + (int) (slash - spec->filename), spec->filename, stem); + } + else + { + snprintf(derivedDir, sizeof(derivedDir), "%s-compose", stem); + } + outDir = derivedDir; + } + + /* Create the output directory */ + if (mkdir(outDir, 0755) != 0 && errno != EEXIST) + { + log_error("mkdir \"%s\": %m", outDir); + return false; + } + + runner_init(&r, spec, outDir); + + /* Generate SSL certs if the cluster ssl mode requires them (verify-ca/full) */ + if (!compose_gen_write_ssl_certs(&spec->cluster, outDir)) + { + return false; + } + + /* Write docker-compose.yml (with pgaftest service) */ + if (!compose_gen_write(&spec->cluster, r.composeFile, + r.projectName, r.contextDir, r.specFile)) + { + return false; + } + + /* Write monitor.ini */ + if (spec->cluster.withMonitor) + { + if (!compose_gen_write_monitor_ini(&spec->cluster, outDir)) + { + return false; + } + } + + if (!compose_gen_write_second_monitor_ini(&spec->cluster, outDir)) + { + return false; + } + + /* Write per-node ini files */ + int globalNodeId = 0; + for (int fi = 0; fi < spec->cluster.formationCount; fi++) + { + const TestFormation *form = &spec->cluster.formations[fi]; + for (int ni = 0; ni < form->nodeCount; ni++) + { + if (!compose_gen_write_node_ini(&spec->cluster, form, + &form->nodes[ni], + ++globalNodeId, outDir)) + { + return false; + } + } + } + + /* Write Makefile */ + char makefilePath[1280]; + snprintf(makefilePath, sizeof(makefilePath), "%s/Makefile", outDir); + FILE *mf = fopen(makefilePath, "w"); + if (!mf) + { + log_error("Failed to open \"%s\": %m", makefilePath); + return false; + } + fprintf(mf, + ".PHONY: test down\n" + "\n" + "test:\n" + "\tdocker compose -p %s -f docker-compose.yml" + " up --build --exit-code-from pgaftest --attach pgaftest\n" + "\n" + "down:\n" + "\tdocker compose -p %s -f docker-compose.yml" + " down --volumes --remove-orphans\n", + r.projectName, r.projectName); + fclose(mf); + log_info("Wrote Makefile to \"%s\"", makefilePath); + + /* Print the docker compose command to stdout */ + printf("cd %s && \\\n", outDir); + printf(" docker compose -p %s -f docker-compose.yml" + " up --build --exit-code-from pgaftest --attach pgaftest\n", + r.projectName); + + return true; +} diff --git a/src/bin/pgaftest/test_runner.h b/src/bin/pgaftest/test_runner.h new file mode 100644 index 000000000..7199bd7a4 --- /dev/null +++ b/src/bin/pgaftest/test_runner.h @@ -0,0 +1,100 @@ +/* + * src/bin/pgaftest/test_runner.h + * Test execution engine for .pgaf specs. + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the PostgreSQL License. + */ + +#ifndef TEST_RUNNER_H +#define TEST_RUNNER_H + +#include +#include "test_spec.h" +#include "pgsql.h" + +/* Runtime context for a running test */ +typedef struct TestRunner +{ + TestSpec *spec; + char projectName[64]; /* docker compose project name */ + char composeFile[1024]; /* path to generated compose YAML */ + char composeBase[1152]; /* "docker compose -p name [-f file]" */ + char workDir[1024]; /* temp dir for this run */ + char contextDir[1024]; /* absolute path used as docker build context */ + char specFile[1024]; /* absolute path to the .pgaf spec file */ + + /* TAP counters and buffered output */ + int tapTotal; + int tapPass; + int tapFail; + char tapBuffer[65536]; + int tapBufferLen; + + /* last SQL result (for CMD_EXPECT / CMD_EXPECT_ERROR) */ + char lastSqlOutput[4096]; + bool lastSqlFailed; /* true when last sql raised an error */ + char lastSqlState[8]; /* SQLSTATE from last failed sql */ + char lastSqlService[64]; /* service name of last sql command */ + + bool composeUp; /* compose stack is running */ + + /* + * Direct libpq connection to the monitor's exposed postgres port. + * Used for LISTEN "state" notifications so wait loops are event-driven + * instead of polling via docker exec. + */ + PGSQL notifyConn; + bool notifyConnected; + + /* + * Which monitor service the runner currently targets for LISTEN/NOTIFY + * and monitor_get_node_state() queries. Defaults to "monitor"; changed + * by the "set monitor " DSL command in a replace-monitor test. + */ + char activeMonitorService[64]; +} TestRunner; + +/* + * CI mode: run the full spec (setup → sequence → teardown). + * Emits TAP to stdout. Returns true if all steps passed. + */ + +/* + * noCleanup: when true, skip `compose down` after the run so the stack stays + * up for post-mortem inspection. Use `pgaftest down ` to clean up. + */ +bool runner_run(TestSpec *spec, const char *workDir, bool noCleanup); + +/* + * Interactive mode: bring up compose, run setup{}, then stop. + * The caller gets the running cluster to explore interactively. + * If withTmux is true, launch a tmux session instead of returning. + */ +bool runner_setup(TestSpec *spec, const char *workDir, bool withTmux); + +/* + * Run a single named step against an already-running compose stack. + * Used by `pgaftest step `. + */ +bool runner_step(TestSpec *spec, const char *workDir, const char *stepName); + +/* + * Tear down the compose stack (run teardown{} then compose down). + * Used by `pgaftest down`. + */ +bool runner_down(TestSpec *spec, const char *workDir); + +/* + * Print the generated docker-compose.yml without starting anything. + */ +bool runner_show(TestSpec *spec); + +/* + * Prepare an output directory with docker-compose.yml, *.ini files, and a + * Makefile. Prints the `docker compose up` command to stdout. + * outDir is created if it does not exist. Pass NULL to derive from spec name. + */ +bool runner_prepare(TestSpec *spec, const char *outDir); + +#endif /* TEST_RUNNER_H */ diff --git a/src/bin/pgaftest/test_spec.h b/src/bin/pgaftest/test_spec.h new file mode 100644 index 000000000..4446de914 --- /dev/null +++ b/src/bin/pgaftest/test_spec.h @@ -0,0 +1,256 @@ +/* + * src/bin/pgaftest/test_spec.h + * AST types for the .pgaf test specification format. + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the PostgreSQL License. + */ + +#ifndef TEST_SPEC_H +#define TEST_SPEC_H + +#include +#include "pgsetup.h" /* PgInstanceKind */ + +#define PGAF_MAX_NODES 32 +#define PGAF_MAX_FORMATIONS 16 +#define PGAF_MAX_NODE_VOLUMES 8 +#define PGAF_MAX_STEPS 256 +#define PGAF_MAX_SEQ 256 +#define PGAF_TIMEOUT_DEFAULT 90 + +/* ----------------------------------------------------------------------- + * Cluster topology (from the cluster { } block) + * + * Hierarchy: cluster → monitor + formations → nodes + * + * Syntax: + * + * cluster { + * monitor [port 15432] + * image "pg_auto_failover:pg17" # optional; default: build from src + * ssl self-signed # optional; default: self-signed + * auth trust # optional; default: trust + * + * formation [name] [num-sync N] { + * node1 + * node2 + * node3 async candidate-priority 0 + * coord coordinator + * w1 worker group 1 + * } + * + * # Multiple formations (Citus use case) + * formation workers num-sync 1 { + * w1 worker group 1 + * w2 worker group 1 + * } + * } + * + * When "formation" has no name it defaults to "default". + * When a formation block is omitted entirely the cluster is invalid. + * ----------------------------------------------------------------------- */ + +typedef struct TestNode +{ + char name[128]; + PgInstanceKind kind; /* standalone / coordinator / worker */ + int group; /* Citus group id; 0 = coordinator */ + int candidatePriority; /* 0-100, default 50 */ + bool replicationQuorum; /* participates in sync quorum */ + bool async; /* async standby (replicationQuorum = false) */ + + /* create-time options — passed to pg_autoctl create via [options] ini */ + bool noMonitor; /* --no-monitor: standalone node */ + bool launchDeferred; /* node waits for pg_autoctl node start */ + bool listen; /* --listen 0.0.0.0: bind all interfaces */ + bool citusSecondary; /* --citus-secondary */ + char citusClusterName[64]; /* --citus-cluster-name NAME */ + int pgPort; /* --pg-port N (0 = default 5432) */ + char debianCluster[64]; /* --debian-cluster NAME */ + char ssl[32]; /* per-node ssl override; "" = use cluster */ + char auth[32]; /* per-node auth override; "" = use cluster */ + char replicationPassword[256]; /* replication.password written to node ini */ + char monitorPassword[256]; /* pg_auto_failover.monitor_password written to node ini */ + + /* Extra Docker named volumes: volume */ + struct + { + char name[64]; + char path[256]; + } volumes[PGAF_MAX_NODE_VOLUMES]; + int volumeCount; +} TestNode; + +typedef struct TestFormation +{ + char name[128]; /* formation name; default "default" */ + int numSync; /* number-sync-standbys, -1 = unset */ + TestNode nodes[PGAF_MAX_NODES]; + int nodeCount; +} TestFormation; + +typedef struct TestCluster +{ + TestFormation formations[PGAF_MAX_FORMATIONS]; + int formationCount; + + bool withMonitor; /* true when "monitor" keyword appears in cluster{} */ + bool withCitus; + bool bindSource; /* bind-source: mount repo root → /usr/src/pg_auto_failover */ + + /* cluster-level Docker / network options */ + char image[256]; /* Docker image tag; "" = build from source */ + char ssl[32]; /* self-signed | cert | off; default self-signed */ + char auth[32]; /* trust | md5 | scram; default trust */ + char monitorPassword[256]; /* password for autoctl_node; embedded in monitor URI */ + int monitorHostPort; /* random free host port mapped to monitor:5432 */ + char extensionVersion[64]; /* PG_AUTOCTL_EXTENSION_VERSION env var for monitor */ + + /* + * Optional second monitor, used for replace-monitor tests. + * When set, compose_gen emits a second monitor service with the given name + * and (if initiallyStoped) stops it after compose up so that the test can + * start it at the right moment. + */ + char secondMonitorName[64]; /* "" when no second monitor */ + bool secondMonitorStopped; /* true when declared with "launch deferred" */ + int secondMonitorHostPort; /* random free host port for second monitor */ + + char monitorDebianCluster[64]; /* debian-cluster name for the monitor, "" otherwise */ + char monitorImageTarget[64]; /* Dockerfile build target for monitor; "" = "run" */ +} TestCluster; + +/* ----------------------------------------------------------------------- + * Commands (inside step/setup/teardown blocks) + * ----------------------------------------------------------------------- */ + +/* Maximum number of states in a multi-state wait */ +#define PGAF_MAX_WAIT_STATES 8 + +/* Maximum number of nodes in a promote list */ +#define PGAF_MAX_PROMOTE_NODES 16 + +/* Maximum number of groups in a formation wait */ +#define PGAF_MAX_WAIT_GROUPS 16 + +typedef enum TestCmdKind +{ + CMD_EXEC, /* exec */ + CMD_EXEC_FAILS, /* exec-fails */ + CMD_WAIT_STATE, /* wait until state = [timeout Ns] */ + CMD_WAIT_STATES, /* wait until s1, s2 [in group N,...] [timeout Ns] */ + CMD_ASSERT_STATE, /* assert state = */ + CMD_ASSERT_ASSIGNED, /* assert assigned-state = */ + CMD_PROMOTE, /* promote node1 [, node2, ...] */ + CMD_SQL, /* sql { SQL } */ + CMD_EXPECT, /* expect { text } */ + CMD_EXPECT_ERROR, /* expect error [SQLSTATE] */ + CMD_NETWORK_OFF, /* network disconnect */ + CMD_NETWORK_ON, /* network connect */ + CMD_SLEEP, /* sleep Ns */ + CMD_COMPOSE_DOWN, /* compose down */ + CMD_COMPOSE_START, /* compose start */ + CMD_COMPOSE_STOP, /* compose stop */ + CMD_COMPOSE_KILL, /* compose kill (SIGKILL, no grace) */ + CMD_WAIT_STOPPED, /* wait until stopped [timeout Ns] */ + CMD_WAIT_MULTI, /* wait until n1 state is s1 and n2 state is s2 */ + CMD_PG_AUTOCTL, /* pg_autoctl — runs in pgaftest container */ + CMD_STOP_POSTGRES, /* stop postgres — pg_autoctl manual pgctl off */ + CMD_START_POSTGRES, /* start postgres — pg_autoctl manual pgctl on */ + CMD_STAYS_WHILE, /* assert stays while { cmds } */ + CMD_SET_MONITOR, /* set monitor — switch active monitor service */ + CMD_LOGS_CHECK, /* logs [not] — grep container logs */ + CMD_COMPOSE_INJECT, /* compose inject : */ +} TestCmdKind; + +typedef struct TestCmd +{ + TestCmdKind kind; + char service[64]; /* target service / node name */ + char args[4096]; /* exec args or SQL text */ + char state[64]; /* expected state / SQLSTATE */ + char expected[4096]; /* for CMD_EXPECT */ + int timeoutSeconds; /* for CMD_WAIT_STATE / CMD_WAIT_STATES */ + bool allowError; /* CMD_SQL: don't fail if SQL errors */ + + /* CMD_WAIT_STATES: list of states that must ALL be present */ + char waitStates[PGAF_MAX_WAIT_STATES][64]; + int waitStateCount; + + /* CMD_WAIT_STATES: group filter; -1 = all groups */ + int waitGroups[PGAF_MAX_WAIT_GROUPS]; + int waitGroupCount; /* 0 means no group filter (all groups) */ + + /* CMD_WAIT_MULTI: per-node conditions (parallel arrays) */ + char waitNodes[PGAF_MAX_WAIT_STATES][64]; + + /* waitStates[] re-used for the per-node state; waitStateCount is the count */ + + /* CMD_PROMOTE: list of nodes to make primary in their respective groups */ + char promoteNodes[PGAF_MAX_PROMOTE_NODES][64]; + int promoteCount; + + /* + * CMD_WAIT_STATE / CMD_WAIT_MULTI: + * Optional list of intermediate states the node must pass through before + * reaching the target state. Checked via LISTEN/NOTIFY on the monitor. + */ + char passThroughStates[PGAF_MAX_WAIT_STATES][64]; + int passThroughCount; + + /* CMD_STAYS_WHILE: nested command list (the "while { }" body) */ + struct TestCmd *body; + + /* CMD_LOGS_CHECK */ + bool logsNegate; /* true → assert pattern NOT found */ + + struct TestCmd *next; +} TestCmd; + +/* ----------------------------------------------------------------------- + * Named step + * ----------------------------------------------------------------------- */ + +typedef struct TestStep +{ + char name[128]; + TestCmd *commands; /* linked list */ + struct TestStep *next; +} TestStep; + +/* ----------------------------------------------------------------------- + * Full test specification + * ----------------------------------------------------------------------- */ + +typedef struct TestSpec +{ + char filename[1024]; + + TestCluster cluster; + + TestStep *setup; /* single anonymous step */ + TestStep *teardown; /* single anonymous step */ + + TestStep *steps; /* named steps, linked list */ + int stepCount; + + char *sequence[PGAF_MAX_SEQ]; /* ordered step names */ + int sequenceLength; +} TestSpec; + +/* ----------------------------------------------------------------------- + * Parser entry point (implemented by bison grammar) + * ----------------------------------------------------------------------- */ + +extern TestSpec * parse_test_spec(const char *filename); + +/* ----------------------------------------------------------------------- + * Helpers + * ----------------------------------------------------------------------- */ + +TestStep * spec_find_step(TestSpec *spec, const char *name); +TestCmd * make_cmd(TestCmdKind kind); +TestStep * make_step(const char *name); + +#endif /* TEST_SPEC_H */ diff --git a/src/bin/pgaftest/test_spec_parse.c b/src/bin/pgaftest/test_spec_parse.c new file mode 100644 index 000000000..4d52d2e70 --- /dev/null +++ b/src/bin/pgaftest/test_spec_parse.c @@ -0,0 +1,4074 @@ +/* A Bison parser, made by GNU Bison 2.3. */ + +/* Skeleton implementation for Bison's Yacc-like parsers in C + * + * Copyright (C) 1984, 1989, 1990, 2000, 2001, 2002, 2003, 2004, 2005, 2006 + * Free Software Foundation, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. */ + +/* As a special exception, you may create a larger work that contains + * part or all of the Bison parser skeleton and distribute that work + * under terms of your choice, so long as that work isn't itself a + * parser generator using the skeleton or a modified version thereof + * as a parser skeleton. Alternatively, if you modify or redistribute + * the parser skeleton itself, you may (at your option) remove this + * special exception, which will cause the skeleton and the resulting + * Bison output files to be licensed under the GNU General Public + * License without this special exception. + * + * This special exception was added by the Free Software Foundation in + * version 2.2 of Bison. */ + +/* C LALR(1) parser skeleton written by Richard Stallman, by + * simplifying the original so-called "semantic" parser. */ + +/* All symbols defined below should begin with yy or YY, to avoid + * infringing on user name space. This should be done even for local + * variables, as they might otherwise be expanded by user macros. + * There are some unavoidable exceptions within include files to + * define necessary library symbols; they are noted "INFRINGES ON + * USER NAME SPACE" below. */ + +/* Identify Bison output. */ +#define YYBISON 1 + +/* Bison version. */ +#define YYBISON_VERSION "2.3" + +/* Skeleton name. */ +#define YYSKELETON_NAME "yacc.c" + +/* Pure parsers. */ +#define YYPURE 0 + +/* Using locations. */ +#define YYLSP_NEEDED 0 + + +/* Tokens. */ +#ifndef YYTOKENTYPE +# define YYTOKENTYPE + +/* Put the tokens into the symbol table, so that GDB and other debuggers + * know about them. */ +enum yytokentype +{ + T_CLUSTER = 258, + T_MONITOR = 259, + T_NODE = 260, + T_CITUS_COORDINATOR = 261, + T_CITUS_WORKER = 262, + T_SETUP = 263, + T_TEARDOWN = 264, + T_STEP = 265, + T_SEQUENCE = 266, + T_EQUALS = 267, + T_IMAGE = 268, + T_IMAGE_TARGET = 269, + T_SSL = 270, + T_AUTH = 271, + T_AUTH_METHOD = 272, + T_FORMATION = 273, + T_NUM_SYNC = 274, + T_COORDINATOR = 275, + T_WORKER = 276, + T_ASYNC = 277, + T_NO_MONITOR = 278, + T_LAUNCH = 279, + T_DEFERRED = 280, + T_IMMEDIATE = 281, + T_INITIALLY = 282, + T_VOLUME = 283, + T_LISTEN = 284, + T_CITUS_SECONDARY = 285, + T_CANDIDATE_PRIORITY = 286, + T_PORT = 287, + T_PASSWORD = 288, + T_MONITOR_PASSWORD = 289, + T_CITUS_CLUSTER_NAME = 290, + T_DEBIAN_CLUSTER = 291, + T_REPLICATION_QUORUM = 292, + T_REPLICATION_PASSWORD = 293, + T_EXTENSION_VERSION = 294, + T_BIND_SOURCE = 295, + T_FS_INIT = 296, + T_FS_SINGLE = 297, + T_FS_PRIMARY = 298, + T_FS_WAIT_PRIMARY = 299, + T_FS_WAIT_STANDBY = 300, + T_FS_DEMOTED = 301, + T_FS_DEMOTE_TIMEOUT = 302, + T_FS_DRAINING = 303, + T_FS_SECONDARY = 304, + T_FS_CATCHINGUP = 305, + T_FS_PREP_PROMOTION = 306, + T_FS_STOP_REPLICATION = 307, + T_FS_MAINTENANCE = 308, + T_FS_JOIN_PRIMARY = 309, + T_FS_APPLY_SETTINGS = 310, + T_FS_PREPARE_MAINTENANCE = 311, + T_FS_WAIT_MAINTENANCE = 312, + T_FS_REPORT_LSN = 313, + T_FS_FAST_FORWARD = 314, + T_FS_JOIN_SECONDARY = 315, + T_FS_DROPPED = 316, + T_EXEC = 317, + T_EXEC_FAILS = 318, + T_PG_AUTOCTL = 319, + T_WAIT = 320, + T_UNTIL = 321, + T_TIMEOUT = 322, + T_AND = 323, + T_IS = 324, + T_WITH = 325, + T_ASSERT = 326, + T_SQL = 327, + T_EXPECT = 328, + T_ERROR = 329, + T_PROMOTE = 330, + T_NETWORK = 331, + T_DISCONNECT = 332, + T_CONNECT = 333, + T_SLEEP = 334, + T_COMPOSE = 335, + T_DOWN = 336, + T_START = 337, + T_STOP = 338, + T_STOPPED = 339, + T_KILL = 340, + T_INJECT = 341, + T_STATE = 342, + T_ASSIGNED_STATE = 343, + T_IN = 344, + T_GROUP = 345, + T_LBRACE = 346, + T_RBRACE = 347, + T_COMMA = 348, + T_POSTGRES = 349, + T_STAYS = 350, + T_WHILE = 351, + T_THROUGH = 352, + T_SET = 353, + T_LOGS = 354, + T_NOT = 355, + T_CONTAINS = 356, + T_MATCHES = 357, + T_INTEGER = 358, + T_IDENT = 359, + T_STRING = 360, + T_BLOCK = 361, + T_SHELL_ARGS = 362 +}; +#endif + +/* Tokens. */ +#define T_CLUSTER 258 +#define T_MONITOR 259 +#define T_NODE 260 +#define T_CITUS_COORDINATOR 261 +#define T_CITUS_WORKER 262 +#define T_SETUP 263 +#define T_TEARDOWN 264 +#define T_STEP 265 +#define T_SEQUENCE 266 +#define T_EQUALS 267 +#define T_IMAGE 268 +#define T_IMAGE_TARGET 269 +#define T_SSL 270 +#define T_AUTH 271 +#define T_AUTH_METHOD 272 +#define T_FORMATION 273 +#define T_NUM_SYNC 274 +#define T_COORDINATOR 275 +#define T_WORKER 276 +#define T_ASYNC 277 +#define T_NO_MONITOR 278 +#define T_LAUNCH 279 +#define T_DEFERRED 280 +#define T_IMMEDIATE 281 +#define T_INITIALLY 282 +#define T_VOLUME 283 +#define T_LISTEN 284 +#define T_CITUS_SECONDARY 285 +#define T_CANDIDATE_PRIORITY 286 +#define T_PORT 287 +#define T_PASSWORD 288 +#define T_MONITOR_PASSWORD 289 +#define T_CITUS_CLUSTER_NAME 290 +#define T_DEBIAN_CLUSTER 291 +#define T_REPLICATION_QUORUM 292 +#define T_REPLICATION_PASSWORD 293 +#define T_EXTENSION_VERSION 294 +#define T_BIND_SOURCE 295 +#define T_FS_INIT 296 +#define T_FS_SINGLE 297 +#define T_FS_PRIMARY 298 +#define T_FS_WAIT_PRIMARY 299 +#define T_FS_WAIT_STANDBY 300 +#define T_FS_DEMOTED 301 +#define T_FS_DEMOTE_TIMEOUT 302 +#define T_FS_DRAINING 303 +#define T_FS_SECONDARY 304 +#define T_FS_CATCHINGUP 305 +#define T_FS_PREP_PROMOTION 306 +#define T_FS_STOP_REPLICATION 307 +#define T_FS_MAINTENANCE 308 +#define T_FS_JOIN_PRIMARY 309 +#define T_FS_APPLY_SETTINGS 310 +#define T_FS_PREPARE_MAINTENANCE 311 +#define T_FS_WAIT_MAINTENANCE 312 +#define T_FS_REPORT_LSN 313 +#define T_FS_FAST_FORWARD 314 +#define T_FS_JOIN_SECONDARY 315 +#define T_FS_DROPPED 316 +#define T_EXEC 317 +#define T_EXEC_FAILS 318 +#define T_PG_AUTOCTL 319 +#define T_WAIT 320 +#define T_UNTIL 321 +#define T_TIMEOUT 322 +#define T_AND 323 +#define T_IS 324 +#define T_WITH 325 +#define T_ASSERT 326 +#define T_SQL 327 +#define T_EXPECT 328 +#define T_ERROR 329 +#define T_PROMOTE 330 +#define T_NETWORK 331 +#define T_DISCONNECT 332 +#define T_CONNECT 333 +#define T_SLEEP 334 +#define T_COMPOSE 335 +#define T_DOWN 336 +#define T_START 337 +#define T_STOP 338 +#define T_STOPPED 339 +#define T_KILL 340 +#define T_INJECT 341 +#define T_STATE 342 +#define T_ASSIGNED_STATE 343 +#define T_IN 344 +#define T_GROUP 345 +#define T_LBRACE 346 +#define T_RBRACE 347 +#define T_COMMA 348 +#define T_POSTGRES 349 +#define T_STAYS 350 +#define T_WHILE 351 +#define T_THROUGH 352 +#define T_SET 353 +#define T_LOGS 354 +#define T_NOT 355 +#define T_CONTAINS 356 +#define T_MATCHES 357 +#define T_INTEGER 358 +#define T_IDENT 359 +#define T_STRING 360 +#define T_BLOCK 361 +#define T_SHELL_ARGS 362 + + +/* Copy the first part of user declarations. */ +#line 1 "test_spec_parse.y" + +/* + * src/bin/pgaftest/test_spec_parse.y + * Bison grammar for .pgaf test specification files. + * + * The outer structure (cluster, setup, teardown, step, sequence) is + * described here as bison rules. Inside step/setup/teardown bodies + * the flex lexer enters the STEP_BODY exclusive state and returns + * individual tokens for every keyword, identifier, integer, and + * punctuation character — no more hand-written strstr/strtok parsing. + * + * The cluster { } block is now also fully parsed by this grammar. + * The flex lexer enters the CLUSTER_BODY exclusive state when it sees + * the opening '{' after "cluster", returning proper tokens for every + * keyword, value, and punctuation inside. + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the PostgreSQL License. + */ + +#include +#include +#include + +#include "test_spec.h" +#include "pgsetup.h" +#include "file_utils.h" + +/* provided by test_spec_scan.l */ +extern int yylex(void); +extern int pgaf_line_number; +extern FILE *yyin; +extern int pgaf_next_brace_is_while; /* set before T_LBRACE for while body */ + +/* the spec we're building */ +static TestSpec *current_spec = NULL; + +static void +yyerror(const char *msg) +{ + fprintf(stderr, "pgaftest: parse error at line %d: %s\n", + pgaf_line_number, msg); + exit(1); +} + + +/* helpers */ +static void +append_cmd(TestStep *step, TestCmd *cmd) +{ + if (!step->commands) + { + step->commands = cmd; + } + else + { + TestCmd *c = step->commands; + while (c->next) + { + c = c->next; + } + c->next = cmd; + } +} + + +/* + * expand_tuple_expect — convert `{ r1 } { r2 }` tuple syntax into + * the newline-separated form that psql --tuples-only --no-align produces. + */ +static void +expand_tuple_expect(char *buf, int buflen) +{ + const char *p = buf; + while (*p == ' ' || *p == '\t') + { + p++; + } + + if (p[0] != '{' || (p[1] != ' ' && p[1] != '\t')) + { + return; + } + + char tmp[4096] = { 0 }; + int pos = 0; + bool first = true; + + while (*p) + { + while (*p == ' ' || *p == '\t' || *p == '\n') + { + p++; + } + if (*p == '\0') + { + break; + } + if (*p != '{') + { + break; + } + + p++; + while (*p == ' ' || *p == '\t') + { + p++; + } + + char row[1024] = { 0 }; + int ri = 0; + int depth = 1; + while (*p && depth > 0) + { + if (*p == '{') + { + depth++; + } + else if (*p == '}') + { + if (--depth == 0) + { + break; + } + } + if (depth > 0 && ri < (int) sizeof(row) - 1) + { + row[ri++] = *p; + } + p++; + } + if (*p == '}') + { + p++; + } + + while (ri > 0 && (row[ri - 1] == ' ' || row[ri - 1] == '\t')) + { + ri--; + } + row[ri] = '\0'; + + if (!first && pos < (int) sizeof(tmp) - 1) + { + tmp[pos++] = '\n'; + } + int l = ri; + if (pos + l >= (int) sizeof(tmp)) + { + l = (int) sizeof(tmp) - pos - 1; + } + if (l > 0) + { + memcpy(tmp + pos, row, l); + pos += l; + } + tmp[pos] = '\0'; + first = false; + } + + if (!first) + { + strncpy(buf, tmp, buflen - 1); + } +} + + +static void +register_step(TestSpec *spec, TestStep *step) +{ + if (!spec->steps) + { + spec->steps = step; + } + else + { + TestStep *s = spec->steps; + while (s->next) + { + s = s->next; + } + s->next = step; + } + spec->stepCount++; +} + + +/* ----------------------------------------------------------------------- + * Static state used by multi-element grammar rules. + * + * The parser is single-threaded; these are only live during the reduction + * of a single rule so there is no re-entrancy concern. + * ----------------------------------------------------------------------- */ + +static TestCmd *current_wait_cmd = NULL; +static TestCmd *current_promote_cmd = NULL; +static TestCmd *current_pass_cmd = NULL; /* for opt_passing_through */ +static TestFormation *current_formation = NULL; +static TestNode *current_node = NULL; + + +/* Enabling traces. */ +#ifndef YYDEBUG +# define YYDEBUG 0 +#endif + +/* Enabling verbose error messages. */ +#ifdef YYERROR_VERBOSE +# undef YYERROR_VERBOSE +# define YYERROR_VERBOSE 1 +#else +# define YYERROR_VERBOSE 0 +#endif + +/* Enabling the token table. */ +#ifndef YYTOKEN_TABLE +# define YYTOKEN_TABLE 0 +#endif + +#if !defined YYSTYPE && !defined YYSTYPE_IS_DECLARED +typedef union YYSTYPE +#line 145 "test_spec_parse.y" +{ + int ival; + char *str; + TestStep *step; + TestCmd *cmd; +} + +/* Line 193 of yacc.c. */ +#line 461 "test_spec_parse.c" +YYSTYPE; +# define yystype YYSTYPE /* obsolescent; will be withdrawn */ +# define YYSTYPE_IS_DECLARED 1 +# define YYSTYPE_IS_TRIVIAL 1 +#endif + + +/* Copy the second part of user declarations. */ + + +/* Line 216 of yacc.c. */ +#line 474 "test_spec_parse.c" + +#ifdef short +# undef short +#endif + +#ifdef YYTYPE_UINT8 +typedef YYTYPE_UINT8 yytype_uint8; +#else +typedef unsigned char yytype_uint8; +#endif + +#ifdef YYTYPE_INT8 +typedef YYTYPE_INT8 yytype_int8; +#elif (defined __STDC__ || defined __C99__FUNC__ \ + || defined __cplusplus || defined _MSC_VER) +typedef signed char yytype_int8; +#else +typedef short int yytype_int8; +#endif + +#ifdef YYTYPE_UINT16 +typedef YYTYPE_UINT16 yytype_uint16; +#else +typedef unsigned short int yytype_uint16; +#endif + +#ifdef YYTYPE_INT16 +typedef YYTYPE_INT16 yytype_int16; +#else +typedef short int yytype_int16; +#endif + +#ifndef YYSIZE_T +# ifdef __SIZE_TYPE__ +# define YYSIZE_T __SIZE_TYPE__ +# elif defined size_t +# define YYSIZE_T size_t +# elif !defined YYSIZE_T && (defined __STDC__ || defined __C99__FUNC__ \ + || defined __cplusplus || defined _MSC_VER) +# include /* INFRINGES ON USER NAME SPACE */ +# define YYSIZE_T size_t +# else +# define YYSIZE_T unsigned int +# endif +#endif + +#define YYSIZE_MAXIMUM ((YYSIZE_T) -1) + +#ifndef YY_ +# if defined YYENABLE_NLS && YYENABLE_NLS +# if ENABLE_NLS +# include /* INFRINGES ON USER NAME SPACE */ +# define YY_(msgid) dgettext("bison-runtime", msgid) +# endif +# endif +# ifndef YY_ +# define YY_(msgid) msgid +# endif +#endif + +/* Suppress unused-variable warnings by "using" E. */ +#if !defined lint || defined __GNUC__ +# define YYUSE(e) ((void) (e)) +#else +# define YYUSE(e) /* empty */ +#endif + +/* Identity function, used to suppress warnings about constant conditions. */ +#ifndef lint +# define YYID(n) (n) +#else +#if (defined __STDC__ || defined __C99__FUNC__ \ + || defined __cplusplus || defined _MSC_VER) +static int YYID(int i) +#else +static int YYID(i) +int i; +#endif +{ + return i; +} +#endif + +#if !defined yyoverflow || YYERROR_VERBOSE + +/* The parser invokes alloca or malloc; define the necessary symbols. */ + +# ifdef YYSTACK_USE_ALLOCA +# if YYSTACK_USE_ALLOCA +# ifdef __GNUC__ +# define YYSTACK_ALLOC __builtin_alloca +# elif defined __BUILTIN_VA_ARG_INCR +# include /* INFRINGES ON USER NAME SPACE */ +# elif defined _AIX +# define YYSTACK_ALLOC __alloca +# elif defined _MSC_VER +# include /* INFRINGES ON USER NAME SPACE */ +# define alloca _alloca +# else +# define YYSTACK_ALLOC alloca +# if !defined _ALLOCA_H && !defined _STDLIB_H && (defined __STDC__ || \ + defined __C99__FUNC__ \ + || defined __cplusplus || \ + defined _MSC_VER) +# include /* INFRINGES ON USER NAME SPACE */ +# ifndef _STDLIB_H +# define _STDLIB_H 1 +# endif +# endif +# endif +# endif +# endif + +# ifdef YYSTACK_ALLOC + +/* Pacify GCC's `empty if-body' warning. */ +# define YYSTACK_FREE(Ptr) do { /* empty */; } while (YYID(0)) +# ifndef YYSTACK_ALLOC_MAXIMUM + +/* The OS might guarantee only one guard page at the bottom of the stack, + * and a page size can be as small as 4096 bytes. So we cannot safely + * invoke alloca (N) if N exceeds 4096. Use a slightly smaller number + * to allow for a few compiler-allocated temporary stack slots. */ +# define YYSTACK_ALLOC_MAXIMUM 4032 /* reasonable circa 2006 */ +# endif +# else +# define YYSTACK_ALLOC YYMALLOC +# define YYSTACK_FREE YYFREE +# ifndef YYSTACK_ALLOC_MAXIMUM +# define YYSTACK_ALLOC_MAXIMUM YYSIZE_MAXIMUM +# endif +# if (defined __cplusplus && !defined _STDLIB_H \ + && !((defined YYMALLOC || defined malloc) \ + && (defined YYFREE || defined free))) +# include /* INFRINGES ON USER NAME SPACE */ +# ifndef _STDLIB_H +# define _STDLIB_H 1 +# endif +# endif +# ifndef YYMALLOC +# define YYMALLOC malloc +# if !defined malloc && !defined _STDLIB_H && (defined __STDC__ || defined __C99__FUNC__ \ + || defined __cplusplus || \ + defined _MSC_VER) +void * malloc(YYSIZE_T); /* INFRINGES ON USER NAME SPACE */ +# endif +# endif +# ifndef YYFREE +# define YYFREE free +# if !defined free && !defined _STDLIB_H && (defined __STDC__ || defined __C99__FUNC__ \ + || defined __cplusplus || defined _MSC_VER) +void free(void *); /* INFRINGES ON USER NAME SPACE */ +# endif +# endif +# endif +#endif /* ! defined yyoverflow || YYERROR_VERBOSE */ + + +#if (!defined yyoverflow \ + && (!defined __cplusplus \ + || (defined YYSTYPE_IS_TRIVIAL && YYSTYPE_IS_TRIVIAL))) + +/* A type that is properly aligned for any stack member. */ +union yyalloc +{ + yytype_int16 yyss; + YYSTYPE yyvs; +}; + +/* The size of the maximum gap between one aligned stack and the next. */ +# define YYSTACK_GAP_MAXIMUM (sizeof(union yyalloc) - 1) + +/* The size of an array large to enough to hold all stacks, each with + * N elements. */ +# define YYSTACK_BYTES(N) \ + ((N) *(sizeof(yytype_int16) + sizeof(YYSTYPE)) \ + + YYSTACK_GAP_MAXIMUM) + +/* Copy COUNT objects from FROM to TO. The source and destination do + * not overlap. */ +# ifndef YYCOPY +# if defined __GNUC__ && 1 < __GNUC__ +# define YYCOPY(To, From, Count) \ + __builtin_memcpy(To, From, (Count) * sizeof(*(From))) +# else +# define YYCOPY(To, From, Count) \ + do \ + { \ + YYSIZE_T yyi; \ + for (yyi = 0; yyi < (Count); yyi++) { \ + (To)[yyi] = (From)[yyi]; } \ + } \ + while (YYID(0)) +# endif +# endif + +/* Relocate STACK from its old location to the new one. The + * local variables YYSIZE and YYSTACKSIZE give the old and new number of + * elements in the stack, and YYPTR gives the new location of the + * stack. Advance YYPTR to a properly aligned location for the next + * stack. */ +# define YYSTACK_RELOCATE(Stack) \ + do \ + { \ + YYSIZE_T yynewbytes; \ + YYCOPY(&yyptr->Stack, Stack, yysize); \ + Stack = &yyptr->Stack; \ + yynewbytes = yystacksize * sizeof(*Stack) + YYSTACK_GAP_MAXIMUM; \ + yyptr += yynewbytes / sizeof(*yyptr); \ + } \ + while (YYID(0)) + +#endif + +/* YYFINAL -- State number of the termination state. */ +#define YYFINAL 21 + +/* YYLAST -- Last index in YYTABLE. */ +#define YYLAST 523 + +/* YYNTOKENS -- Number of terminals. */ +#define YYNTOKENS 108 + +/* YYNNTS -- Number of nonterminals. */ +#define YYNNTS 62 + +/* YYNRULES -- Number of rules. */ +#define YYNRULES 193 + +/* YYNRULES -- Number of states. */ +#define YYNSTATES 312 + +/* YYTRANSLATE(YYLEX) -- Bison symbol number corresponding to YYLEX. */ +#define YYUNDEFTOK 2 +#define YYMAXUTOK 362 + +#define YYTRANSLATE(YYX) \ + ((unsigned int) (YYX) <= YYMAXUTOK ? yytranslate[YYX] : YYUNDEFTOK) + +/* YYTRANSLATE[YYLEX] -- Bison symbol number corresponding to YYLEX. */ +static const yytype_uint8 yytranslate[] = +{ + 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 1, 2, 3, 4, + 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, + 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, + 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, + 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, + 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, + 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, + 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, + 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, + 105, 106, 107 +}; + +#if YYDEBUG + +/* YYPRHS[YYN] -- Index of the first RHS symbol of rule number YYN in + * YYRHS. */ +static const yytype_uint16 yyprhs[] = +{ + 0, 0, 3, 5, 8, 10, 12, 14, 16, 18, + 19, 25, 26, 29, 31, 33, 35, 37, 39, 41, + 43, 45, 49, 53, 57, 61, 66, 71, 78, 81, + 84, 87, 90, 93, 96, 99, 100, 107, 108, 111, + 113, 115, 117, 119, 121, 123, 126, 127, 130, 132, + 134, 135, 136, 141, 142, 150, 151, 154, 156, 158, + 160, 162, 164, 167, 170, 172, 174, 176, 179, 182, + 185, 188, 191, 194, 197, 200, 203, 206, 209, 213, + 217, 220, 223, 227, 231, 232, 235, 237, 239, 241, + 243, 245, 247, 249, 251, 253, 255, 257, 259, 261, + 265, 268, 272, 275, 279, 282, 284, 286, 288, 293, + 298, 300, 304, 305, 308, 310, 312, 316, 320, 321, + 331, 332, 342, 350, 358, 364, 370, 377, 379, 381, + 385, 389, 390, 393, 396, 401, 402, 405, 409, 416, + 423, 430, 437, 441, 444, 447, 451, 455, 458, 460, + 464, 468, 472, 475, 478, 482, 486, 490, 495, 499, + 503, 504, 510, 516, 520, 525, 531, 536, 542, 545, + 546, 549, 551, 553, 555, 557, 559, 561, 563, 565, + 567, 569, 571, 573, 575, 577, 579, 581, 583, 585, + 587, 589, 591, 593 +}; + +/* YYRHS -- A `-1'-separated list of the rules' RHS. */ +static const yytype_int16 yyrhs[] = +{ + 109, 0, -1, 110, -1, 109, 110, -1, 111, -1, + 133, -1, 134, -1, 135, -1, 166, -1, -1, 3, + 91, 112, 113, 92, -1, -1, 113, 114, -1, 115, + -1, 116, -1, 118, -1, 119, -1, 117, -1, 120, + -1, 40, -1, 4, -1, 4, 36, 104, -1, 4, + 14, 104, -1, 4, 32, 103, -1, 4, 33, 105, + -1, 4, 104, 24, 25, -1, 4, 104, 27, 84, + -1, 4, 104, 24, 25, 33, 105, -1, 13, 105, + -1, 13, 104, -1, 39, 104, -1, 39, 105, -1, + 15, 104, -1, 16, 104, -1, 17, 104, -1, -1, + 18, 121, 122, 91, 125, 92, -1, -1, 122, 124, + -1, 104, -1, 105, -1, 16, -1, 4, -1, 5, + -1, 123, -1, 19, 103, -1, -1, 125, 128, -1, + 104, -1, 4, -1, -1, -1, 126, 127, 129, 131, + -1, -1, 5, 104, 127, 130, 91, 131, 92, -1, + -1, 131, 132, -1, 20, -1, 21, -1, 22, -1, + 23, -1, 25, -1, 24, 25, -1, 24, 26, -1, + 26, -1, 29, -1, 30, -1, 31, 103, -1, 90, + 103, -1, 32, 103, -1, 35, 104, -1, 36, 104, + -1, 15, 104, -1, 16, 104, -1, 17, 104, -1, + 37, 104, -1, 38, 105, -1, 34, 105, -1, 28, + 104, 104, -1, 28, 104, 105, -1, 8, 136, -1, + 9, 136, -1, 10, 169, 136, -1, 91, 137, 92, + -1, -1, 137, 138, -1, 139, -1, 145, -1, 152, + -1, 153, -1, 154, -1, 155, -1, 157, -1, 158, + -1, 159, -1, 160, -1, 163, -1, 164, -1, 165, + -1, 62, 104, 107, -1, 62, 104, -1, 63, 104, + 107, -1, 63, 104, -1, 64, 104, 107, -1, 64, + 104, -1, 64, -1, 12, -1, 69, -1, 104, 87, + 140, 168, -1, 104, 87, 140, 104, -1, 141, -1, + 142, 68, 141, -1, -1, 97, 144, -1, 168, -1, + 104, -1, 144, 93, 168, -1, 144, 93, 104, -1, + -1, 65, 66, 104, 87, 140, 168, 146, 143, 151, + -1, -1, 65, 66, 104, 87, 140, 104, 147, 143, + 151, -1, 65, 66, 104, 88, 140, 168, 151, -1, + 65, 66, 104, 88, 140, 104, 151, -1, 65, 66, + 104, 84, 151, -1, 65, 66, 148, 149, 151, -1, + 65, 66, 141, 68, 142, 151, -1, 168, -1, 104, + -1, 148, 93, 168, -1, 148, 93, 104, -1, -1, + 89, 150, -1, 90, 103, -1, 150, 93, 90, 103, + -1, -1, 67, 103, -1, 70, 67, 103, -1, 71, + 104, 87, 140, 168, 151, -1, 71, 104, 87, 140, + 104, 151, -1, 71, 104, 88, 140, 168, 151, -1, + 71, 104, 88, 140, 104, 151, -1, 72, 104, 106, + -1, 73, 106, -1, 73, 74, -1, 73, 74, 104, + -1, 73, 74, 103, -1, 75, 156, -1, 104, -1, + 156, 93, 104, -1, 76, 77, 104, -1, 76, 78, + 104, -1, 79, 103, -1, 80, 81, -1, 80, 82, + 104, -1, 80, 83, 104, -1, 80, 85, 104, -1, + 80, 86, 104, 107, -1, 83, 94, 126, -1, 82, + 94, 126, -1, -1, 96, 162, 91, 137, 92, -1, + 71, 126, 95, 168, 161, -1, 98, 104, 104, -1, + 99, 104, 101, 105, -1, 99, 104, 100, 101, 105, + -1, 99, 104, 102, 105, -1, 99, 104, 100, 102, + 105, -1, 11, 167, -1, -1, 167, 169, -1, 41, + -1, 42, -1, 43, -1, 44, -1, 45, -1, 46, + -1, 47, -1, 48, -1, 49, -1, 50, -1, 51, + -1, 52, -1, 53, -1, 54, -1, 55, -1, 56, + -1, 57, -1, 58, -1, 59, -1, 60, -1, 61, + -1, 104, -1, 105, -1 +}; + +/* YYRLINE[YYN] -- source line where rule number YYN was defined. */ +static const yytype_uint16 yyrline[] = +{ + 0, 211, 211, 212, 216, 217, 218, 219, 220, 233, + 232, 242, 244, 248, 249, 250, 251, 252, 253, 254, + 267, 271, 278, 285, 291, 298, 305, 312, 325, 331, + 341, 347, 357, 367, 373, 384, 383, 400, 402, 411, + 412, 413, 414, 415, 419, 424, 430, 432, 451, 452, + 461, 478, 477, 485, 484, 492, 494, 498, 503, 508, + 512, 516, 520, 524, 528, 532, 536, 540, 544, 548, + 552, 558, 564, 569, 574, 579, 587, 593, 599, 613, + 634, 641, 652, 670, 685, 688, 696, 697, 698, 699, + 700, 701, 702, 703, 704, 705, 706, 707, 708, 722, + 729, 735, 742, 748, 756, 762, 789, 789, 800, 815, + 833, 834, 849, 851, 855, 863, 871, 878, 890, 889, + 901, 900, 911, 920, 929, 936, 950, 965, 971, 978, + 984, 997, 999, 1003, 1008, 1016, 1017, 1018, 1029, 1037, + 1045, 1053, 1071, 1086, 1093, 1097, 1103, 1116, 1124, 1132, + 1147, 1153, 1166, 1180, 1184, 1190, 1196, 1222, 1256, 1262, + 1279, 1279, 1284, 1303, 1328, 1337, 1346, 1355, 1371, 1374, + 1376, 1398, 1399, 1400, 1401, 1402, 1403, 1404, 1405, 1406, + 1407, 1408, 1409, 1410, 1411, 1412, 1413, 1414, 1415, 1416, + 1417, 1418, 1426, 1427 +}; +#endif + +#if YYDEBUG || YYERROR_VERBOSE || YYTOKEN_TABLE + +/* YYTNAME[SYMBOL-NUM] -- String name of the symbol SYMBOL-NUM. + * First, the terminals, then, starting at YYNTOKENS, nonterminals. */ +static const char *const yytname[] = +{ + "$end", "error", "$undefined", "T_CLUSTER", "T_MONITOR", "T_NODE", + "T_CITUS_COORDINATOR", "T_CITUS_WORKER", "T_SETUP", "T_TEARDOWN", + "T_STEP", "T_SEQUENCE", "T_EQUALS", "T_IMAGE", "T_IMAGE_TARGET", "T_SSL", + "T_AUTH", "T_AUTH_METHOD", "T_FORMATION", "T_NUM_SYNC", "T_COORDINATOR", + "T_WORKER", "T_ASYNC", "T_NO_MONITOR", "T_LAUNCH", "T_DEFERRED", + "T_IMMEDIATE", "T_INITIALLY", "T_VOLUME", "T_LISTEN", + "T_CITUS_SECONDARY", "T_CANDIDATE_PRIORITY", "T_PORT", "T_PASSWORD", + "T_MONITOR_PASSWORD", "T_CITUS_CLUSTER_NAME", "T_DEBIAN_CLUSTER", + "T_REPLICATION_QUORUM", "T_REPLICATION_PASSWORD", "T_EXTENSION_VERSION", + "T_BIND_SOURCE", "T_FS_INIT", "T_FS_SINGLE", "T_FS_PRIMARY", + "T_FS_WAIT_PRIMARY", "T_FS_WAIT_STANDBY", "T_FS_DEMOTED", + "T_FS_DEMOTE_TIMEOUT", "T_FS_DRAINING", "T_FS_SECONDARY", + "T_FS_CATCHINGUP", "T_FS_PREP_PROMOTION", "T_FS_STOP_REPLICATION", + "T_FS_MAINTENANCE", "T_FS_JOIN_PRIMARY", "T_FS_APPLY_SETTINGS", + "T_FS_PREPARE_MAINTENANCE", "T_FS_WAIT_MAINTENANCE", "T_FS_REPORT_LSN", + "T_FS_FAST_FORWARD", "T_FS_JOIN_SECONDARY", "T_FS_DROPPED", "T_EXEC", + "T_EXEC_FAILS", "T_PG_AUTOCTL", "T_WAIT", "T_UNTIL", "T_TIMEOUT", + "T_AND", "T_IS", "T_WITH", "T_ASSERT", "T_SQL", "T_EXPECT", "T_ERROR", + "T_PROMOTE", "T_NETWORK", "T_DISCONNECT", "T_CONNECT", "T_SLEEP", + "T_COMPOSE", "T_DOWN", "T_START", "T_STOP", "T_STOPPED", "T_KILL", + "T_INJECT", "T_STATE", "T_ASSIGNED_STATE", "T_IN", "T_GROUP", "T_LBRACE", + "T_RBRACE", "T_COMMA", "T_POSTGRES", "T_STAYS", "T_WHILE", "T_THROUGH", + "T_SET", "T_LOGS", "T_NOT", "T_CONTAINS", "T_MATCHES", "T_INTEGER", + "T_IDENT", "T_STRING", "T_BLOCK", "T_SHELL_ARGS", "$accept", "spec", + "spec_item", "cluster_block", "@1", "cluster_item_list", "cluster_item", + "monitor_line", "image_line", "extension_version_line", "ssl_line", + "auth_line", "formation_block", "@2", "formation_opt_list", "bare_name", + "formation_opt", "node_list", "node_name", "init_node_slot", "node_line", + "@3", "@4", "node_opt_list", "node_opt", "setup_block", "teardown_block", + "named_step", "cmd_block", "cmd_list", "step_cmd", "exec_cmd", + "state_op", "wait_multi_condition", "wait_multi_condition_list", + "opt_passing_through", "pass_state_list", "wait_cmd", "@5", "@6", + "state_name_list", "opt_in_group", "group_items", "opt_timeout", + "assert_cmd", "sql_cmd", "expect_cmd", "promote_cmd", "promote_list", + "network_cmd", "sleep_cmd", "compose_cmd", "postgres_ctl_cmd", + "while_body", "@7", "stays_while_cmd", "set_monitor_cmd", "logs_cmd", + "sequence_block", "sequence_names", "fsm_state", "ident_or_string", 0 +}; +#endif + +# ifdef YYPRINT + +/* YYTOKNUM[YYLEX-NUM] -- Internal token number corresponding to + * token YYLEX-NUM. */ +static const yytype_uint16 yytoknum[] = +{ + 0, 256, 257, 258, 259, 260, 261, 262, 263, 264, + 265, 266, 267, 268, 269, 270, 271, 272, 273, 274, + 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, + 285, 286, 287, 288, 289, 290, 291, 292, 293, 294, + 295, 296, 297, 298, 299, 300, 301, 302, 303, 304, + 305, 306, 307, 308, 309, 310, 311, 312, 313, 314, + 315, 316, 317, 318, 319, 320, 321, 322, 323, 324, + 325, 326, 327, 328, 329, 330, 331, 332, 333, 334, + 335, 336, 337, 338, 339, 340, 341, 342, 343, 344, + 345, 346, 347, 348, 349, 350, 351, 352, 353, 354, + 355, 356, 357, 358, 359, 360, 361, 362 +}; +# endif + +/* YYR1[YYN] -- Symbol number of symbol that rule YYN derives. */ +static const yytype_uint8 yyr1[] = +{ + 0, 108, 109, 109, 110, 110, 110, 110, 110, 112, + 111, 113, 113, 114, 114, 114, 114, 114, 114, 114, + 115, 115, 115, 115, 115, 115, 115, 115, 116, 116, + 117, 117, 118, 119, 119, 121, 120, 122, 122, 123, + 123, 123, 123, 123, 124, 124, 125, 125, 126, 126, + 127, 129, 128, 130, 128, 131, 131, 132, 132, 132, + 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, + 132, 132, 132, 132, 132, 132, 132, 132, 132, 132, + 133, 134, 135, 136, 137, 137, 138, 138, 138, 138, + 138, 138, 138, 138, 138, 138, 138, 138, 138, 139, + 139, 139, 139, 139, 139, 139, 140, 140, 141, 141, + 142, 142, 143, 143, 144, 144, 144, 144, 146, 145, + 147, 145, 145, 145, 145, 145, 145, 148, 148, 148, + 148, 149, 149, 150, 150, 151, 151, 151, 152, 152, + 152, 152, 153, 154, 154, 154, 154, 155, 156, 156, + 157, 157, 158, 159, 159, 159, 159, 159, 160, 160, + 162, 161, 163, 164, 165, 165, 165, 165, 166, 167, + 167, 168, 168, 168, 168, 168, 168, 168, 168, 168, + 168, 168, 168, 168, 168, 168, 168, 168, 168, 168, + 168, 168, 169, 169 +}; + +/* YYR2[YYN] -- Number of symbols composing right hand side of rule YYN. */ +static const yytype_uint8 yyr2[] = +{ + 0, 2, 1, 2, 1, 1, 1, 1, 1, 0, + 5, 0, 2, 1, 1, 1, 1, 1, 1, 1, + 1, 3, 3, 3, 3, 4, 4, 6, 2, 2, + 2, 2, 2, 2, 2, 0, 6, 0, 2, 1, + 1, 1, 1, 1, 1, 2, 0, 2, 1, 1, + 0, 0, 4, 0, 7, 0, 2, 1, 1, 1, + 1, 1, 2, 2, 1, 1, 1, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, + 2, 2, 3, 3, 0, 2, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, + 2, 3, 2, 3, 2, 1, 1, 1, 4, 4, + 1, 3, 0, 2, 1, 1, 3, 3, 0, 9, + 0, 9, 7, 7, 5, 5, 6, 1, 1, 3, + 3, 0, 2, 2, 4, 0, 2, 3, 6, 6, + 6, 6, 3, 2, 2, 3, 3, 2, 1, 3, + 3, 3, 2, 2, 3, 3, 3, 4, 3, 3, + 0, 5, 5, 3, 4, 5, 4, 5, 2, 0, + 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1 +}; + +/* YYDEFACT[STATE-NAME] -- Default rule to reduce with in state + * STATE-NUM when YYTABLE doesn't specify something else to do. Zero + * means the default is an error. */ +static const yytype_uint8 yydefact[] = +{ + 0, 0, 0, 0, 0, 169, 0, 2, 4, 5, + 6, 7, 8, 9, 84, 80, 81, 192, 193, 0, + 168, 1, 3, 11, 0, 82, 170, 0, 0, 0, + 105, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 83, 0, 0, 85, 86, 87, 88, 89, 90, + 91, 92, 93, 94, 95, 96, 97, 98, 20, 0, + 0, 0, 0, 35, 0, 19, 10, 12, 13, 14, + 17, 15, 16, 18, 100, 102, 104, 0, 49, 48, + 0, 0, 144, 143, 148, 147, 0, 0, 152, 153, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 29, 28, 32, 33, 34, 37, 30, + 31, 99, 101, 103, 171, 172, 173, 174, 175, 176, + 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, + 187, 188, 189, 190, 191, 128, 0, 131, 127, 0, + 0, 0, 142, 146, 145, 0, 150, 151, 154, 155, + 156, 0, 48, 159, 158, 163, 0, 0, 0, 22, + 23, 24, 21, 0, 0, 0, 135, 0, 0, 0, + 0, 0, 135, 106, 107, 0, 0, 0, 149, 157, + 0, 0, 164, 166, 25, 26, 42, 43, 41, 0, + 46, 39, 40, 44, 38, 0, 0, 124, 0, 0, + 0, 110, 135, 0, 132, 130, 129, 125, 135, 135, + 135, 135, 160, 162, 165, 167, 0, 45, 0, 136, + 0, 120, 118, 135, 135, 0, 0, 126, 133, 0, + 139, 138, 141, 140, 0, 27, 0, 36, 50, 47, + 137, 112, 112, 123, 122, 0, 111, 0, 84, 50, + 51, 0, 135, 135, 109, 108, 134, 0, 53, 55, + 115, 113, 114, 121, 119, 161, 0, 52, 0, 55, + 0, 0, 0, 57, 58, 59, 60, 0, 61, 64, + 0, 65, 66, 0, 0, 0, 0, 0, 0, 0, + 0, 56, 117, 116, 0, 72, 73, 74, 62, 63, + 0, 67, 69, 77, 70, 71, 75, 76, 68, 54, + 78, 79 +}; + +/* YYDEFGOTO[NTERM-NUM]. */ +static const yytype_int16 yydefgoto[] = +{ + -1, 6, 7, 8, 23, 27, 67, 68, 69, 70, + 71, 72, 73, 108, 165, 193, 194, 218, 80, 250, + 239, 259, 266, 267, 291, 9, 10, 11, 15, 24, + 44, 45, 175, 136, 202, 252, 261, 46, 242, 241, + 137, 172, 204, 197, 47, 48, 49, 50, 85, 51, + 52, 53, 54, 213, 234, 55, 56, 57, 12, 20, + 138, 19 +}; + +/* YYPACT[STATE-NUM] -- Index in YYTABLE of the portion describing + * STATE-NUM. */ +#define YYPACT_NINF -160 +static const yytype_int16 yypact[] = +{ + 45, -76, -65, -65, -45, -160, 109, -160, -160, -160, + -160, -160, -160, -160, -160, -160, -160, -160, -160, -65, + -45, -160, -160, -160, 145, -160, -160, 6, -72, -64, + -57, -24, 3, -25, -62, -19, 24, -14, 47, 19, + 32, -160, -9, 27, -160, -160, -160, -160, -160, -160, + -160, -160, -160, -160, -160, -160, -160, -160, -5, 11, + 33, 53, 111, -160, 30, -160, -160, -160, -160, -160, + -160, -160, -160, -160, 106, 112, 126, 122, -160, 55, + 29, 128, 102, -160, -160, 58, 127, 131, -160, -160, + 132, 134, 135, 136, 4, 4, 137, 21, 138, 129, + 140, 142, 73, -160, -160, -160, -160, -160, -160, -160, + -160, -160, -160, -160, -160, -160, -160, -160, -160, -160, + -160, -160, -160, -160, -160, -160, -160, -160, -160, -160, + -160, -160, -160, -160, -160, -51, 244, -75, -160, 17, + 17, 440, -160, -160, -160, 213, -160, -160, -160, -160, + -160, 211, -160, -160, -160, -160, 110, 214, 215, -160, + -160, -160, -160, 296, 241, 1, 44, 17, 17, 224, + 239, 143, 44, -160, -160, 207, 228, 240, -160, -160, + 230, 232, -160, -160, 305, -160, -160, -160, -160, 236, + -160, -160, -160, -160, -160, 237, 274, -160, 249, 313, + 255, -160, 20, 242, 253, -160, -160, -160, 44, 44, + 44, 44, -160, -160, -160, -160, 243, -160, -1, -160, + 248, 276, 279, 44, 44, 17, 224, -160, -160, 262, + -160, -160, -160, -160, 327, -160, 315, -160, -160, -160, + -160, 323, 323, -160, -160, 334, -160, 318, -160, -160, + -160, 355, 44, 44, -160, -160, -160, 251, -160, -160, + -160, 329, -160, -160, -160, -160, 332, 124, 419, -160, + 320, 321, 322, -160, -160, -160, -160, 197, -160, -160, + 324, -160, -160, 326, 328, 325, 330, 331, 333, 335, + 336, -160, -160, -160, 46, -160, -160, -160, -160, -160, + 125, -160, -160, -160, -160, -160, -160, -160, -160, -160, + -160, -160 +}; + +/* YYPGOTO[NTERM-NUM]. */ +static const yytype_int16 yypgoto[] = +{ + -160, -160, 421, -160, -160, -160, -160, -160, -160, -160, + -160, -160, -160, -160, -160, -160, -160, -160, -93, 183, + -160, -160, -160, 164, -160, -160, -160, -160, 22, 188, + -160, -160, -129, -153, -160, 199, -160, -160, -160, -160, + -160, -160, -160, -159, -160, -160, -160, -160, -160, -160, + -160, -160, -160, -160, -160, -160, -160, -160, -160, -160, + -141, 422 +}; + +/* YYTABLE[YYPACT[STATE-NUM]]. What to do in state STATE-NUM. If + * positive, shift that token. If negative, reduce the rule which + * number is the opposite. If zero, do what YYDEFACT says. + * If YYTABLE_NINF, syntax error. */ +#define YYTABLE_NINF -110 +static const yytype_int16 yytable[] = +{ + 177, 153, 154, 78, 236, 186, 187, 78, 78, 98, + 58, 176, 82, 207, 170, 13, 201, 188, 171, 59, + 189, 60, 61, 62, 63, 16, 14, 99, 100, 173, + 206, 101, 74, 166, 209, 211, 167, 168, 198, 199, + 75, 25, 77, 227, 83, 64, 65, 76, 1, 230, + 231, 232, 233, 2, 3, 4, 5, 222, 224, 17, + 18, 270, 271, 272, 243, 244, 273, 274, 275, 276, + 277, 278, 279, 246, 280, 281, 282, 283, 284, 81, + 285, 286, 287, 288, 289, 84, 174, 195, 226, 88, + 196, 237, 190, 263, 264, 96, 245, 163, 66, 102, + 164, 86, 87, 152, 255, 191, 192, 79, 152, 21, + 262, 195, 1, 94, 196, 103, 104, 2, 3, 4, + 5, 156, 157, 158, 141, 238, 95, 293, 89, 90, + 91, 97, 92, 93, 109, 110, 290, 105, 309, 270, + 271, 272, 139, 140, 273, 274, 275, 276, 277, 278, + 279, 145, 280, 281, 282, 283, 284, 106, 285, 286, + 287, 288, 289, 114, 115, 116, 117, 118, 119, 120, + 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, + 131, 132, 133, 134, 114, 115, 116, 117, 118, 119, + 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, + 130, 131, 132, 133, 134, 143, 144, 28, 29, 30, + 31, 180, 181, 111, 290, 107, 32, 33, 34, 112, + 35, 36, 298, 299, 37, 38, 135, 39, 40, 310, + 311, 146, 160, 113, 142, 147, 148, 41, 149, 150, + 151, 155, 159, 42, 43, 161, 162, 205, 114, 115, + 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, + 126, 127, 128, 129, 130, 131, 132, 133, 134, 114, + 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, + 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, + 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, + 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, + 134, 208, 169, 28, 29, 30, 31, 178, 179, 182, + 183, 184, 32, 33, 34, 185, 35, 36, 200, 203, + 37, 38, 210, 39, 40, 214, 212, 215, 216, 217, + 219, 220, 225, 265, -109, 228, 229, -108, 235, 42, + 43, 240, 247, 221, 114, 115, 116, 117, 118, 119, + 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, + 130, 131, 132, 133, 134, 114, 115, 116, 117, 118, + 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, + 129, 130, 131, 132, 133, 134, 114, 115, 116, 117, + 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, + 128, 129, 130, 131, 132, 133, 134, 223, 248, 249, + 251, 256, 268, 269, 295, 296, 297, 22, 300, 301, + 303, 302, 258, 294, 304, 305, 257, 306, 254, 308, + 307, 253, 26, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 260, + 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, + 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, + 134, 114, 115, 116, 117, 118, 119, 120, 121, 122, + 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, + 133, 134, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 292 +}; + +static const yytype_int16 yycheck[] = +{ + 141, 94, 95, 4, 5, 4, 5, 4, 4, 14, + 4, 140, 74, 172, 89, 91, 169, 16, 93, 13, + 19, 15, 16, 17, 18, 3, 91, 32, 33, 12, + 171, 36, 104, 84, 175, 176, 87, 88, 167, 168, + 104, 19, 66, 202, 106, 39, 40, 104, 3, 208, + 209, 210, 211, 8, 9, 10, 11, 198, 199, 104, + 105, 15, 16, 17, 223, 224, 20, 21, 22, 23, + 24, 25, 26, 226, 28, 29, 30, 31, 32, 104, + 34, 35, 36, 37, 38, 104, 69, 67, 68, 103, + 70, 92, 91, 252, 253, 104, 225, 24, 92, 104, + 27, 77, 78, 104, 245, 104, 105, 104, 104, 0, + 251, 67, 3, 94, 70, 104, 105, 8, 9, 10, + 11, 100, 101, 102, 95, 218, 94, 268, 81, 82, + 83, 104, 85, 86, 104, 105, 90, 104, 92, 15, + 16, 17, 87, 88, 20, 21, 22, 23, 24, 25, + 26, 93, 28, 29, 30, 31, 32, 104, 34, 35, + 36, 37, 38, 41, 42, 43, 44, 45, 46, 47, + 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, + 58, 59, 60, 61, 41, 42, 43, 44, 45, 46, + 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, + 57, 58, 59, 60, 61, 103, 104, 62, 63, 64, + 65, 101, 102, 107, 90, 104, 71, 72, 73, 107, + 75, 76, 25, 26, 79, 80, 104, 82, 83, 104, + 105, 104, 103, 107, 106, 104, 104, 92, 104, 104, + 104, 104, 104, 98, 99, 105, 104, 104, 41, 42, + 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, + 53, 54, 55, 56, 57, 58, 59, 60, 61, 41, + 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, + 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, + 61, 104, 68, 62, 63, 64, 65, 104, 107, 105, + 105, 25, 71, 72, 73, 84, 75, 76, 104, 90, + 79, 80, 104, 82, 83, 105, 96, 105, 33, 103, + 103, 67, 87, 92, 68, 103, 93, 68, 105, 98, + 99, 103, 90, 104, 41, 42, 43, 44, 45, 46, + 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, + 57, 58, 59, 60, 61, 41, 42, 43, 44, 45, + 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, + 56, 57, 58, 59, 60, 61, 41, 42, 43, 44, + 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, + 55, 56, 57, 58, 59, 60, 61, 104, 91, 104, + 97, 103, 93, 91, 104, 104, 104, 6, 104, 103, + 105, 103, 249, 269, 104, 104, 248, 104, 104, 103, + 105, 242, 20, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, 104, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, + 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, + 61, 41, 42, 43, 44, 45, 46, 47, 48, 49, + 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, + 60, 61, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, 104 +}; + +/* YYSTOS[STATE-NUM] -- The (internal number of the) accessing + * symbol of state STATE-NUM. */ +static const yytype_uint8 yystos[] = +{ + 0, 3, 8, 9, 10, 11, 109, 110, 111, 133, + 134, 135, 166, 91, 91, 136, 136, 104, 105, 169, + 167, 0, 110, 112, 137, 136, 169, 113, 62, 63, + 64, 65, 71, 72, 73, 75, 76, 79, 80, 82, + 83, 92, 98, 99, 138, 139, 145, 152, 153, 154, + 155, 157, 158, 159, 160, 163, 164, 165, 4, 13, + 15, 16, 17, 18, 39, 40, 92, 114, 115, 116, + 117, 118, 119, 120, 104, 104, 104, 66, 4, 104, + 126, 104, 74, 106, 104, 156, 77, 78, 103, 81, + 82, 83, 85, 86, 94, 94, 104, 104, 14, 32, + 33, 36, 104, 104, 105, 104, 104, 104, 121, 104, + 105, 107, 107, 107, 41, 42, 43, 44, 45, 46, + 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, + 57, 58, 59, 60, 61, 104, 141, 148, 168, 87, + 88, 95, 106, 103, 104, 93, 104, 104, 104, 104, + 104, 104, 104, 126, 126, 104, 100, 101, 102, 104, + 103, 105, 104, 24, 27, 122, 84, 87, 88, 68, + 89, 93, 149, 12, 69, 140, 140, 168, 104, 107, + 101, 102, 105, 105, 25, 84, 4, 5, 16, 19, + 91, 104, 105, 123, 124, 67, 70, 151, 140, 140, + 104, 141, 142, 90, 150, 104, 168, 151, 104, 168, + 104, 168, 96, 161, 105, 105, 33, 103, 125, 103, + 67, 104, 168, 104, 168, 87, 68, 151, 103, 93, + 151, 151, 151, 151, 162, 105, 5, 92, 126, 128, + 103, 147, 146, 151, 151, 140, 141, 90, 91, 104, + 127, 97, 143, 143, 104, 168, 103, 137, 127, 129, + 104, 144, 168, 151, 151, 92, 130, 131, 93, 91, + 15, 16, 17, 20, 21, 22, 23, 24, 25, 26, + 28, 29, 30, 31, 32, 34, 35, 36, 37, 38, + 90, 132, 104, 168, 131, 104, 104, 104, 25, 26, + 104, 103, 103, 105, 104, 104, 104, 105, 103, 92, + 104, 105 +}; + +#define yyerrok (yyerrstatus = 0) +#define yyclearin (yychar = YYEMPTY) +#define YYEMPTY (-2) +#define YYEOF 0 + +#define YYACCEPT goto yyacceptlab +#define YYABORT goto yyabortlab +#define YYERROR goto yyerrorlab + + +/* Like YYERROR except do call yyerror. This remains here temporarily + * to ease the transition to the new meaning of YYERROR, for GCC. + * Once GCC version 2 has supplanted version 1, this can go. */ + +#define YYFAIL goto yyerrlab + +#define YYRECOVERING() (!!yyerrstatus) + +#define YYBACKUP(Token, Value) \ + do \ + if (yychar == YYEMPTY && yylen == 1) \ + { \ + yychar = (Token); \ + yylval = (Value); \ + yytoken = YYTRANSLATE(yychar); \ + YYPOPSTACK(1); \ + goto yybackup; \ + } \ + else \ + { \ + yyerror(YY_("syntax error: cannot back up")); \ + YYERROR; \ + } \ + while (YYID(0)) + + +#define YYTERROR 1 +#define YYERRCODE 256 + + +/* YYLLOC_DEFAULT -- Set CURRENT to span from RHS[1] to RHS[N]. + * If N is 0, then set CURRENT to the empty location which ends + * the previous symbol: RHS[0] (always defined). */ + +#define YYRHSLOC(Rhs, K) ((Rhs)[K]) +#ifndef YYLLOC_DEFAULT +# define YYLLOC_DEFAULT(Current, Rhs, N) \ + do \ + if (YYID(N)) \ + { \ + (Current).first_line = YYRHSLOC(Rhs, 1).first_line; \ + (Current).first_column = YYRHSLOC(Rhs, 1).first_column; \ + (Current).last_line = YYRHSLOC(Rhs, N).last_line; \ + (Current).last_column = YYRHSLOC(Rhs, N).last_column; \ + } \ + else \ + { \ + (Current).first_line = (Current).last_line = \ + YYRHSLOC(Rhs, 0).last_line; \ + (Current).first_column = (Current).last_column = \ + YYRHSLOC(Rhs, 0).last_column; \ + } \ + while (YYID(0)) +#endif + + +/* YY_LOCATION_PRINT -- Print the location on the stream. + * This macro was not mandated originally: define only if we know + * we won't break user code: when these are the locations we know. */ + +#ifndef YY_LOCATION_PRINT +# if defined YYLTYPE_IS_TRIVIAL && YYLTYPE_IS_TRIVIAL +# define YY_LOCATION_PRINT(File, Loc) \ + fprintf(File, "%d.%d-%d.%d", \ + (Loc).first_line, (Loc).first_column, \ + (Loc).last_line, (Loc).last_column) +# else +# define YY_LOCATION_PRINT(File, Loc) ((void) 0) +# endif +#endif + + +/* YYLEX -- calling `yylex' with the right arguments. */ + +#ifdef YYLEX_PARAM +# define YYLEX yylex(YYLEX_PARAM) +#else +# define YYLEX yylex() +#endif + +/* Enable debugging if requested. */ +#if YYDEBUG + +# ifndef YYFPRINTF +# include /* INFRINGES ON USER NAME SPACE */ +# define YYFPRINTF fprintf +# endif + +# define YYDPRINTF(Args) \ + do { \ + if (yydebug) { \ + YYFPRINTF Args; } \ + } while (YYID(0)) + +# define YY_SYMBOL_PRINT(Title, Type, Value, Location) \ + do { \ + if (yydebug) \ + { \ + YYFPRINTF(stderr, "%s ", Title); \ + yy_symbol_print(stderr, \ + Type, Value); \ + YYFPRINTF(stderr, "\n"); \ + } \ + } while (YYID(0)) + + +/*--------------------------------. + | Print this symbol on YYOUTPUT. | + | `--------------------------------*/ + +/*ARGSUSED*/ +#if (defined __STDC__ || defined __C99__FUNC__ \ + || defined __cplusplus || defined _MSC_VER) +static void yy_symbol_value_print(FILE *yyoutput, int yytype, YYSTYPE const *const + yyvaluep) +#else +static void yy_symbol_value_print(yyoutput, yytype, yyvaluep) +FILE *yyoutput; +int yytype; +YYSTYPE const *const yyvaluep; +#endif +{ + if (!yyvaluep) + { + return; + } +# ifdef YYPRINT + if (yytype < YYNTOKENS) + { + YYPRINT(yyoutput, yytoknum[yytype], *yyvaluep); + } +# else + YYUSE(yyoutput); +# endif + switch (yytype) + { + default: + { + break; + } + } +} + + +/*--------------------------------. + | Print this symbol on YYOUTPUT. | + | `--------------------------------*/ + +#if (defined __STDC__ || defined __C99__FUNC__ \ + || defined __cplusplus || defined _MSC_VER) +static void yy_symbol_print(FILE *yyoutput, int yytype, YYSTYPE const *const yyvaluep) +#else +static void yy_symbol_print(yyoutput, yytype, yyvaluep) +FILE *yyoutput; +int yytype; +YYSTYPE const *const yyvaluep; +#endif +{ + if (yytype < YYNTOKENS) + { + YYFPRINTF(yyoutput, "token %s (", yytname[yytype]); + } + else + { + YYFPRINTF(yyoutput, "nterm %s (", yytname[yytype]); + } + + yy_symbol_value_print(yyoutput, yytype, yyvaluep); + YYFPRINTF(yyoutput, ")"); +} + +/*------------------------------------------------------------------. + | yy_stack_print -- Print the state stack from its BOTTOM up to its | + | TOP (included). | + | `------------------------------------------------------------------*/ + +#if (defined __STDC__ || defined __C99__FUNC__ \ + || defined __cplusplus || defined _MSC_VER) +static void yy_stack_print(yytype_int16 *bottom, yytype_int16 *top) +#else +static void yy_stack_print(bottom, top) +yytype_int16 *bottom; +yytype_int16 *top; +#endif +{ + YYFPRINTF(stderr, "Stack now"); + for (; bottom <= top; ++bottom) + { + YYFPRINTF(stderr, " %d", *bottom); + } + YYFPRINTF(stderr, "\n"); +} + +# define YY_STACK_PRINT(Bottom, Top) \ + do { \ + if (yydebug) { \ + yy_stack_print((Bottom), (Top)); } \ + } while (YYID(0)) + + +/*------------------------------------------------. + | Report that the YYRULE is going to be reduced. | + | `------------------------------------------------*/ + +#if (defined __STDC__ || defined __C99__FUNC__ \ + || defined __cplusplus || defined _MSC_VER) +static void yy_reduce_print(YYSTYPE *yyvsp, int yyrule) +#else +static void yy_reduce_print(yyvsp, yyrule) +YYSTYPE *yyvsp; +int yyrule; +#endif +{ + int yynrhs = yyr2[yyrule]; + int yyi; + unsigned long int yylno = yyrline[yyrule]; + YYFPRINTF(stderr, "Reducing stack by rule %d (line %lu):\n", + yyrule - 1, yylno); + + /* The symbols being reduced. */ + for (yyi = 0; yyi < yynrhs; yyi++) + { + fprintf(stderr, " $%d = ", yyi + 1); + yy_symbol_print(stderr, yyrhs[yyprhs[yyrule] + yyi], + &(yyvsp[(yyi + 1) - (yynrhs)]) + ); + fprintf(stderr, "\n"); + } +} + +# define YY_REDUCE_PRINT(Rule) \ + do { \ + if (yydebug) { \ + yy_reduce_print(yyvsp, Rule); } \ + } while (YYID(0)) + +/* Nonzero means print parse trace. It is left uninitialized so that + * multiple parsers can coexist. */ +int yydebug; +#else /* !YYDEBUG */ +# define YYDPRINTF(Args) +# define YY_SYMBOL_PRINT(Title, Type, Value, Location) +# define YY_STACK_PRINT(Bottom, Top) +# define YY_REDUCE_PRINT(Rule) +#endif /* !YYDEBUG */ + + +/* YYINITDEPTH -- initial size of the parser's stacks. */ +#ifndef YYINITDEPTH +# define YYINITDEPTH 200 +#endif + +/* YYMAXDEPTH -- maximum size the stacks can grow to (effective only + * if the built-in stack extension method is used). + * + * Do not make this value too large; the results are undefined if + * YYSTACK_ALLOC_MAXIMUM < YYSTACK_BYTES (YYMAXDEPTH) + * evaluated with infinite-precision integer arithmetic. */ + +#ifndef YYMAXDEPTH +# define YYMAXDEPTH 10000 +#endif + + +#if YYERROR_VERBOSE + +# ifndef yystrlen +# if defined __GLIBC__ && defined _STRING_H +# define yystrlen strlen +# else + +/* Return the length of YYSTR. */ +#if (defined __STDC__ || defined __C99__FUNC__ \ + || defined __cplusplus || defined _MSC_VER) +static YYSIZE_T yystrlen(const char *yystr) +#else +static YYSIZE_T yystrlen(yystr) +const char *yystr; +#endif +{ + YYSIZE_T yylen; + for (yylen = 0; yystr[yylen]; yylen++) + { + continue; + } + return yylen; +} +# endif +# endif + +# ifndef yystpcpy +# if defined __GLIBC__ && defined _STRING_H && defined _GNU_SOURCE +# define yystpcpy stpcpy +# else + +/* Copy YYSRC to YYDEST, returning the address of the terminating '\0' in + * YYDEST. */ +#if (defined __STDC__ || defined __C99__FUNC__ \ + || defined __cplusplus || defined _MSC_VER) +static char * yystpcpy(char *yydest, const char *yysrc) +#else +static char * yystpcpy(yydest, yysrc) +char *yydest; +const char *yysrc; +#endif +{ + char *yyd = yydest; + const char *yys = yysrc; + + while ((*yyd++ = *yys++) != '\0') + { + continue; + } + + return yyd - 1; +} +# endif +# endif + +# ifndef yytnamerr + +/* Copy to YYRES the contents of YYSTR after stripping away unnecessary + * quotes and backslashes, so that it's suitable for yyerror. The + * heuristic is that double-quoting is unnecessary unless the string + * contains an apostrophe, a comma, or backslash (other than + * backslash-backslash). YYSTR is taken from yytname. If YYRES is + * null, do not copy; instead, return the length of what the result + * would have been. */ +static YYSIZE_T +yytnamerr(char *yyres, const char *yystr) +{ + if (*yystr == '"') + { + YYSIZE_T yyn = 0; + char const *yyp = yystr; + + for (;;) + { + switch (*++yyp) + { + case '\'': + case ',': + { + goto do_not_strip_quotes; + } + + case '\\': + { + if (*++yyp != '\\') + { + goto do_not_strip_quotes; + } + + /* Fall through. */ + } + + default: + { + if (yyres) + { + yyres[yyn] = *yyp; + } + yyn++; + break; + } + + case '"': + if (yyres) + { + yyres[yyn] = '\0'; + } + return yyn; + } + } +do_not_strip_quotes:; + } + + if (!yyres) + { + return yystrlen(yystr); + } + + return yystpcpy(yyres, yystr) - yyres; +} + + +# endif + +/* Copy into YYRESULT an error message about the unexpected token + * YYCHAR while in state YYSTATE. Return the number of bytes copied, + * including the terminating null byte. If YYRESULT is null, do not + * copy anything; just return the number of bytes that would be + * copied. As a special case, return 0 if an ordinary "syntax error" + * message will do. Return YYSIZE_MAXIMUM if overflow occurs during + * size calculation. */ +static YYSIZE_T +yysyntax_error(char *yyresult, int yystate, int yychar) +{ + int yyn = yypact[yystate]; + + if (!(YYPACT_NINF < yyn && yyn <= YYLAST)) + { + return 0; + } + else + { + int yytype = YYTRANSLATE(yychar); + YYSIZE_T yysize0 = yytnamerr(0, yytname[yytype]); + YYSIZE_T yysize = yysize0; + YYSIZE_T yysize1; + int yysize_overflow = 0; + enum + { + YYERROR_VERBOSE_ARGS_MAXIMUM = 5 + }; + char const *yyarg[YYERROR_VERBOSE_ARGS_MAXIMUM]; + int yyx; + +# if 0 + + /* This is so xgettext sees the translatable formats that are + * constructed on the fly. */ + YY_("syntax error, unexpected %s"); + YY_("syntax error, unexpected %s, expecting %s"); + YY_("syntax error, unexpected %s, expecting %s or %s"); + YY_("syntax error, unexpected %s, expecting %s or %s or %s"); + YY_("syntax error, unexpected %s, expecting %s or %s or %s or %s"); +# endif + char *yyfmt; + char const *yyf; + static char const yyunexpected[] = "syntax error, unexpected %s"; + static char const yyexpecting[] = ", expecting %s"; + static char const yyor[] = " or %s"; + char yyformat[sizeof yyunexpected + + sizeof yyexpecting - 1 + + ((YYERROR_VERBOSE_ARGS_MAXIMUM - 2) * + (sizeof yyor - 1))]; + char const *yyprefix = yyexpecting; + + /* Start YYX at -YYN if negative to avoid negative indexes in + * YYCHECK. */ + int yyxbegin = yyn < 0 ? -yyn : 0; + + /* Stay within bounds of both yycheck and yytname. */ + int yychecklim = YYLAST - yyn + 1; + int yyxend = yychecklim < YYNTOKENS ? yychecklim : YYNTOKENS; + int yycount = 1; + + yyarg[0] = yytname[yytype]; + yyfmt = yystpcpy(yyformat, yyunexpected); + + for (yyx = yyxbegin; yyx < yyxend; ++yyx) + { + if (yycheck[yyx + yyn] == yyx && yyx != YYTERROR) + { + if (yycount == YYERROR_VERBOSE_ARGS_MAXIMUM) + { + yycount = 1; + yysize = yysize0; + yyformat[sizeof yyunexpected - 1] = '\0'; + break; + } + yyarg[yycount++] = yytname[yyx]; + yysize1 = yysize + yytnamerr(0, yytname[yyx]); + yysize_overflow |= (yysize1 < yysize); + yysize = yysize1; + yyfmt = yystpcpy(yyfmt, yyprefix); + yyprefix = yyor; + } + } + + yyf = YY_(yyformat); + yysize1 = yysize + yystrlen(yyf); + yysize_overflow |= (yysize1 < yysize); + yysize = yysize1; + + if (yysize_overflow) + { + return YYSIZE_MAXIMUM; + } + + if (yyresult) + { + /* Avoid sprintf, as that infringes on the user's name space. + * Don't have undefined behavior even if the translation + * produced a string with the wrong number of "%s"s. */ + char *yyp = yyresult; + int yyi = 0; + while ((*yyp = *yyf) != '\0') + { + if (*yyp == '%' && yyf[1] == 's' && yyi < yycount) + { + yyp += yytnamerr(yyp, yyarg[yyi++]); + yyf += 2; + } + else + { + yyp++; + yyf++; + } + } + } + return yysize; + } +} + + +#endif /* YYERROR_VERBOSE */ + + +/*-----------------------------------------------. + | Release the memory associated to this symbol. | + | `-----------------------------------------------*/ + +/*ARGSUSED*/ +#if (defined __STDC__ || defined __C99__FUNC__ \ + || defined __cplusplus || defined _MSC_VER) +static void yydestruct(const char *yymsg, int yytype, YYSTYPE *yyvaluep) +#else +static void yydestruct(yymsg, yytype, yyvaluep) +const char *yymsg; +int yytype; +YYSTYPE *yyvaluep; +#endif +{ + YYUSE(yyvaluep); + + if (!yymsg) + { + yymsg = "Deleting"; + } + YY_SYMBOL_PRINT(yymsg, yytype, yyvaluep, yylocationp); + + switch (yytype) + { + default: + { + break; + } + } +} + + +/* Prevent warnings from -Wmissing-prototypes. */ + +#ifdef YYPARSE_PARAM +#if defined __STDC__ || defined __cplusplus +int yyparse(void *YYPARSE_PARAM); +#else +int yyparse(); +#endif +#else /* ! YYPARSE_PARAM */ +#if defined __STDC__ || defined __cplusplus +int yyparse(void); +#else +int yyparse(); +#endif +#endif /* ! YYPARSE_PARAM */ + + +/* The look-ahead symbol. */ +int yychar; + +/* The semantic value of the look-ahead symbol. */ +YYSTYPE yylval; + +/* Number of syntax errors so far. */ +int yynerrs; + + +/*----------. + | yyparse. | + | `----------*/ + +#ifdef YYPARSE_PARAM +#if (defined __STDC__ || defined __C99__FUNC__ \ + || defined __cplusplus || defined _MSC_VER) +int yyparse(void *YYPARSE_PARAM) +#else +int yyparse(YYPARSE_PARAM) +void *YYPARSE_PARAM; +#endif +#else /* ! YYPARSE_PARAM */ +#if (defined __STDC__ || defined __C99__FUNC__ \ + || defined __cplusplus || defined _MSC_VER) +int +yyparse(void) +#else +int +yyparse() + +#endif +#endif +{ + int yystate; + int yyn; + int yyresult; + + /* Number of tokens to shift before error messages enabled. */ + int yyerrstatus; + + /* Look-ahead token as an internal (translated) token number. */ + int yytoken = 0; +#if YYERROR_VERBOSE + + /* Buffer for error messages, and its allocated size. */ + char yymsgbuf[128]; + char *yymsg = yymsgbuf; + YYSIZE_T yymsg_alloc = sizeof yymsgbuf; +#endif + + /* Three stacks and their tools: + * `yyss': related to states, + * `yyvs': related to semantic values, + * `yyls': related to locations. + * + * Refer to the stacks thru separate pointers, to allow yyoverflow + * to reallocate them elsewhere. */ + + /* The state stack. */ + yytype_int16 yyssa[YYINITDEPTH]; + yytype_int16 *yyss = yyssa; + yytype_int16 *yyssp; + + /* The semantic value stack. */ + YYSTYPE yyvsa[YYINITDEPTH]; + YYSTYPE *yyvs = yyvsa; + YYSTYPE *yyvsp; + + +#define YYPOPSTACK(N) (yyvsp -= (N), yyssp -= (N)) + + YYSIZE_T yystacksize = YYINITDEPTH; + + /* The variables used to return semantic value and location from the + * action routines. */ + YYSTYPE yyval; + + + /* The number of symbols on the RHS of the reduced rule. + * Keep to zero when no symbol should be popped. */ + int yylen = 0; + + YYDPRINTF((stderr, "Starting parse\n")); + + yystate = 0; + yyerrstatus = 0; + yynerrs = 0; + yychar = YYEMPTY; /* Cause a token to be read. */ + + /* Initialize stack pointers. + * Waste one element of value and location stack + * so that they stay on the same level as the state stack. + * The wasted elements are never initialized. */ + + yyssp = yyss; + yyvsp = yyvs; + + goto yysetstate; + +/*------------------------------------------------------------. + | yynewstate -- Push a new state, which is found in yystate. | + | `------------------------------------------------------------*/ +yynewstate: + + /* In all cases, when you get here, the value and location stacks + * have just been pushed. So pushing a state here evens the stacks. */ + yyssp++; + +yysetstate: + *yyssp = yystate; + + if (yyss + yystacksize - 1 <= yyssp) + { + /* Get the current used size of the three stacks, in elements. */ + YYSIZE_T yysize = yyssp - yyss + 1; + +#ifdef yyoverflow + { + /* Give user a chance to reallocate the stack. Use copies of + * these so that the &'s don't force the real ones into + * memory. */ + YYSTYPE *yyvs1 = yyvs; + yytype_int16 *yyss1 = yyss; + + + /* Each stack pointer address is followed by the size of the + * data in use in that stack, in bytes. This used to be a + * conditional around just the two extra args, but that might + * be undefined if yyoverflow is a macro. */ + yyoverflow(YY_("memory exhausted"), + &yyss1, yysize * sizeof(*yyssp), + &yyvs1, yysize * sizeof(*yyvsp), + + &yystacksize); + + yyss = yyss1; + yyvs = yyvs1; + } +#else /* no yyoverflow */ +# ifndef YYSTACK_RELOCATE + goto yyexhaustedlab; +# else + + /* Extend the stack our own way. */ + if (YYMAXDEPTH <= yystacksize) + { + goto yyexhaustedlab; + } + yystacksize *= 2; + if (YYMAXDEPTH < yystacksize) + { + yystacksize = YYMAXDEPTH; + } + + { + yytype_int16 *yyss1 = yyss; + union yyalloc *yyptr = + (union yyalloc *) YYSTACK_ALLOC(YYSTACK_BYTES(yystacksize)); + if (!yyptr) + { + goto yyexhaustedlab; + } + YYSTACK_RELOCATE(yyss); + YYSTACK_RELOCATE(yyvs); + +# undef YYSTACK_RELOCATE + if (yyss1 != yyssa) + { + YYSTACK_FREE(yyss1); + } + } +# endif +#endif /* no yyoverflow */ + + yyssp = yyss + yysize - 1; + yyvsp = yyvs + yysize - 1; + + + YYDPRINTF((stderr, "Stack size increased to %lu\n", + (unsigned long int) yystacksize)); + + if (yyss + yystacksize - 1 <= yyssp) + { + YYABORT; + } + } + + YYDPRINTF((stderr, "Entering state %d\n", yystate)); + + goto yybackup; + +/*-----------. + | yybackup. | + | `-----------*/ +yybackup: + + /* Do appropriate processing given the current state. Read a + * look-ahead token if we need one and don't already have one. */ + + /* First try to decide what to do without reference to look-ahead token. */ + yyn = yypact[yystate]; + if (yyn == YYPACT_NINF) + { + goto yydefault; + } + + /* Not known => get a look-ahead token if don't already have one. */ + + /* YYCHAR is either YYEMPTY or YYEOF or a valid look-ahead symbol. */ + if (yychar == YYEMPTY) + { + YYDPRINTF((stderr, "Reading a token: ")); + yychar = YYLEX; + } + + if (yychar <= YYEOF) + { + yychar = yytoken = YYEOF; + YYDPRINTF((stderr, "Now at end of input.\n")); + } + else + { + yytoken = YYTRANSLATE(yychar); + YY_SYMBOL_PRINT("Next token is", yytoken, &yylval, &yylloc); + } + + /* If the proper action on seeing token YYTOKEN is to reduce or to + * detect an error, take that action. */ + yyn += yytoken; + if (yyn < 0 || YYLAST < yyn || yycheck[yyn] != yytoken) + { + goto yydefault; + } + yyn = yytable[yyn]; + if (yyn <= 0) + { + if (yyn == 0 || yyn == YYTABLE_NINF) + { + goto yyerrlab; + } + yyn = -yyn; + goto yyreduce; + } + + if (yyn == YYFINAL) + { + YYACCEPT; + } + + /* Count tokens shifted since error; after three, turn off error + * status. */ + if (yyerrstatus) + { + yyerrstatus--; + } + + /* Shift the look-ahead token. */ + YY_SYMBOL_PRINT("Shifting", yytoken, &yylval, &yylloc); + + /* Discard the shifted token unless it is eof. */ + if (yychar != YYEOF) + { + yychar = YYEMPTY; + } + + yystate = yyn; + *++yyvsp = yylval; + + goto yynewstate; + + +/*-----------------------------------------------------------. + | yydefault -- do the default action for the current state. | + | `-----------------------------------------------------------*/ +yydefault: + yyn = yydefact[yystate]; + if (yyn == 0) + { + goto yyerrlab; + } + goto yyreduce; + + +/*-----------------------------. + | yyreduce -- Do a reduction. | + | `-----------------------------*/ +yyreduce: + + /* yyn is the number of a rule to reduce with. */ + yylen = yyr2[yyn]; + + /* If YYLEN is nonzero, implement the default value of the action: + * `$$ = $1'. + * + * Otherwise, the following line sets YYVAL to garbage. + * This behavior is undocumented and Bison + * users should not rely upon it. Assigning to YYVAL + * unconditionally makes the parser a bit smaller, and it avoids a + * GCC warning that YYVAL may be used uninitialized. */ + yyval = yyvsp[1 - yylen]; + + + YY_REDUCE_PRINT(yyn); + switch (yyn) + { + case 9: +#line 233 "test_spec_parse.y" + { + strlcpy(current_spec->cluster.ssl, "self-signed", + sizeof(current_spec->cluster.ssl)); + strlcpy(current_spec->cluster.auth, "trust", + sizeof(current_spec->cluster.auth)); + } + break; + + case 19: +#line 254 "test_spec_parse.y" + { + current_spec->cluster.bindSource = true; + } + break; + + case 20: +#line 268 "test_spec_parse.y" + { + current_spec->cluster.withMonitor = true; + } + break; + + case 21: +#line 272 "test_spec_parse.y" + { + current_spec->cluster.withMonitor = true; + strlcpy(current_spec->cluster.monitorDebianCluster, (yyvsp[(3) - + (3)].str), + sizeof(current_spec->cluster.monitorDebianCluster)); + free((yyvsp[(3) - (3)].str)); + } + break; + + case 22: +#line 279 "test_spec_parse.y" + { + current_spec->cluster.withMonitor = true; + strlcpy(current_spec->cluster.monitorImageTarget, (yyvsp[(3) - (3)].str), + sizeof(current_spec->cluster.monitorImageTarget)); + free((yyvsp[(3) - (3)].str)); + } + break; + + case 23: +#line 286 "test_spec_parse.y" + { + current_spec->cluster.withMonitor = true; + + /* monitor port not stored in TestCluster yet; ignore */ + (void) (yyvsp[(3) - (3)].ival); + } + break; + + case 24: +#line 292 "test_spec_parse.y" + { + current_spec->cluster.withMonitor = true; + strlcpy(current_spec->cluster.monitorPassword, (yyvsp[(3) - (3)].str), + sizeof(current_spec->cluster.monitorPassword)); + free((yyvsp[(3) - (3)].str)); + } + break; + + case 25: +#line 299 "test_spec_parse.y" + { + strlcpy(current_spec->cluster.secondMonitorName, (yyvsp[(2) - (4)].str), + sizeof(current_spec->cluster.secondMonitorName)); + current_spec->cluster.secondMonitorStopped = true; + free((yyvsp[(2) - (4)].str)); + } + break; + + case 26: +#line 306 "test_spec_parse.y" + { + strlcpy(current_spec->cluster.secondMonitorName, (yyvsp[(2) - (4)].str), + sizeof(current_spec->cluster.secondMonitorName)); + current_spec->cluster.secondMonitorStopped = true; + free((yyvsp[(2) - (4)].str)); + } + break; + + case 27: +#line 313 "test_spec_parse.y" + { + strlcpy(current_spec->cluster.secondMonitorName, (yyvsp[(2) - (6)].str), + sizeof(current_spec->cluster.secondMonitorName)); + current_spec->cluster.secondMonitorStopped = true; + free((yyvsp[(2) - (6)].str)); + + /* password for second monitor not yet stored */ + free((yyvsp[(6) - (6)].str)); + } + break; + + case 28: +#line 326 "test_spec_parse.y" + { + strlcpy(current_spec->cluster.image, (yyvsp[(2) - (2)].str), + sizeof(current_spec->cluster.image)); + free((yyvsp[(2) - (2)].str)); + } + break; + + case 29: +#line 332 "test_spec_parse.y" + { + strlcpy(current_spec->cluster.image, (yyvsp[(2) - (2)].str), + sizeof(current_spec->cluster.image)); + free((yyvsp[(2) - (2)].str)); + } + break; + + case 30: +#line 342 "test_spec_parse.y" + { + strlcpy(current_spec->cluster.extensionVersion, (yyvsp[(2) - (2)].str), + sizeof(current_spec->cluster.extensionVersion)); + free((yyvsp[(2) - (2)].str)); + } + break; + + case 31: +#line 348 "test_spec_parse.y" + { + strlcpy(current_spec->cluster.extensionVersion, (yyvsp[(2) - (2)].str), + sizeof(current_spec->cluster.extensionVersion)); + free((yyvsp[(2) - (2)].str)); + } + break; + + case 32: +#line 358 "test_spec_parse.y" + { + strlcpy(current_spec->cluster.ssl, (yyvsp[(2) - (2)].str), + sizeof(current_spec->cluster.ssl)); + free((yyvsp[(2) - (2)].str)); + } + break; + + case 33: +#line 368 "test_spec_parse.y" + { + strlcpy(current_spec->cluster.auth, (yyvsp[(2) - (2)].str), + sizeof(current_spec->cluster.auth)); + free((yyvsp[(2) - (2)].str)); + } + break; + + case 34: +#line 374 "test_spec_parse.y" + { + strlcpy(current_spec->cluster.auth, (yyvsp[(2) - (2)].str), + sizeof(current_spec->cluster.auth)); + free((yyvsp[(2) - (2)].str)); + } + break; + + case 35: +#line 384 "test_spec_parse.y" + { + TestCluster *cl = ¤t_spec->cluster; + if (cl->formationCount >= PGAF_MAX_FORMATIONS) + { + fprintf(stderr, "pgaftest: too many formations (max %d)\n", + PGAF_MAX_FORMATIONS); + exit(1); + } + current_formation = &cl->formations[cl->formationCount++]; + strlcpy(current_formation->name, "default", + sizeof(current_formation->name)); + current_formation->numSync = -1; + } + break; + + case 39: +#line 411 "test_spec_parse.y" + { + (yyval.str) = (yyvsp[(1) - (1)].str); + } + break; + + case 40: +#line 412 "test_spec_parse.y" + { + (yyval.str) = (yyvsp[(1) - (1)].str); + } + break; + + case 41: +#line 413 "test_spec_parse.y" + { + (yyval.str) = strdup("auth"); + } + break; + + case 42: +#line 414 "test_spec_parse.y" + { + (yyval.str) = strdup("monitor"); + } + break; + + case 43: +#line 415 "test_spec_parse.y" + { + (yyval.str) = strdup("node"); + } + break; + + case 44: +#line 420 "test_spec_parse.y" + { + strlcpy(current_formation->name, (yyvsp[(1) - (1)].str), + sizeof(current_formation->name)); + free((yyvsp[(1) - (1)].str)); + } + break; + + case 45: +#line 425 "test_spec_parse.y" + { + current_formation->numSync = (yyvsp[(2) - (2)].ival); + } + break; + + case 48: +#line 451 "test_spec_parse.y" + { + (yyval.str) = (yyvsp[(1) - (1)].str); + } + break; + + case 49: +#line 452 "test_spec_parse.y" + { + (yyval.str) = strdup("monitor"); + } + break; + + case 50: +#line 461 "test_spec_parse.y" + { + if (current_formation->nodeCount >= PGAF_MAX_NODES) + { + fprintf(stderr, "pgaftest: too many nodes in formation (max %d)\n", + PGAF_MAX_NODES); + exit(1); + } + current_node = ¤t_formation->nodes[current_formation->nodeCount++]; + current_node->kind = NODE_KIND_STANDALONE; + current_node->candidatePriority = 50; + current_node->replicationQuorum = true; + } + break; + + case 51: +#line 478 "test_spec_parse.y" + { + strlcpy(current_node->name, (yyvsp[(1) - (2)].str), + sizeof(current_node->name)); + free((yyvsp[(1) - (2)].str)); + } + break; + + case 53: +#line 485 "test_spec_parse.y" + { + strlcpy(current_node->name, (yyvsp[(2) - (3)].str), + sizeof(current_node->name)); + free((yyvsp[(2) - (3)].str)); + } + break; + + case 57: +#line 499 "test_spec_parse.y" + { + current_node->kind = NODE_KIND_CITUS_COORDINATOR; + current_spec->cluster.withCitus = true; + } + break; + + case 58: +#line 504 "test_spec_parse.y" + { + current_node->kind = NODE_KIND_CITUS_WORKER; + current_spec->cluster.withCitus = true; + } + break; + + case 59: +#line 509 "test_spec_parse.y" + { + current_node->replicationQuorum = false; + } + break; + + case 60: +#line 513 "test_spec_parse.y" + { + current_node->noMonitor = true; + } + break; + + case 61: +#line 517 "test_spec_parse.y" + { + current_node->launchDeferred = true; + } + break; + + case 62: +#line 521 "test_spec_parse.y" + { + current_node->launchDeferred = true; + } + break; + + case 63: +#line 525 "test_spec_parse.y" + { + current_node->launchDeferred = false; + } + break; + + case 64: +#line 529 "test_spec_parse.y" + { + current_node->launchDeferred = false; + } + break; + + case 65: +#line 533 "test_spec_parse.y" + { + current_node->listen = true; + } + break; + + case 66: +#line 537 "test_spec_parse.y" + { + current_node->citusSecondary = true; + } + break; + + case 67: +#line 541 "test_spec_parse.y" + { + current_node->candidatePriority = (yyvsp[(2) - (2)].ival); + } + break; + + case 68: +#line 545 "test_spec_parse.y" + { + current_node->group = (yyvsp[(2) - (2)].ival); + } + break; + + case 69: +#line 549 "test_spec_parse.y" + { + current_node->pgPort = (yyvsp[(2) - (2)].ival); + } + break; + + case 70: +#line 553 "test_spec_parse.y" + { + strlcpy(current_node->citusClusterName, (yyvsp[(2) - (2)].str), + sizeof(current_node->citusClusterName)); + free((yyvsp[(2) - (2)].str)); + } + break; + + case 71: +#line 559 "test_spec_parse.y" + { + strlcpy(current_node->debianCluster, (yyvsp[(2) - (2)].str), + sizeof(current_node->debianCluster)); + free((yyvsp[(2) - (2)].str)); + } + break; + + case 72: +#line 565 "test_spec_parse.y" + { + strlcpy(current_node->ssl, (yyvsp[(2) - (2)].str), + sizeof(current_node->ssl)); + free((yyvsp[(2) - (2)].str)); + } + break; + + case 73: +#line 570 "test_spec_parse.y" + { + strlcpy(current_node->auth, (yyvsp[(2) - (2)].str), + sizeof(current_node->auth)); + free((yyvsp[(2) - (2)].str)); + } + break; + + case 74: +#line 575 "test_spec_parse.y" + { + strlcpy(current_node->auth, (yyvsp[(2) - (2)].str), + sizeof(current_node->auth)); + free((yyvsp[(2) - (2)].str)); + } + break; + + case 75: +#line 580 "test_spec_parse.y" + { + if (strcmp((yyvsp[(2) - (2)].str), "false") == 0 || strcmp((yyvsp[(2) - + (2)].str), + "0") == 0) + { + current_node->replicationQuorum = false; + } + else + { + current_node->replicationQuorum = true; + } + free((yyvsp[(2) - (2)].str)); + } + break; + + case 76: +#line 588 "test_spec_parse.y" + { + strlcpy(current_node->replicationPassword, (yyvsp[(2) - (2)].str), + sizeof(current_node->replicationPassword)); + free((yyvsp[(2) - (2)].str)); + } + break; + + case 77: +#line 594 "test_spec_parse.y" + { + strlcpy(current_node->monitorPassword, (yyvsp[(2) - (2)].str), + sizeof(current_node->monitorPassword)); + free((yyvsp[(2) - (2)].str)); + } + break; + + case 78: +#line 600 "test_spec_parse.y" + { + /* volume — adds a named Docker volume */ + int vi = current_node->volumeCount; + if (vi < PGAF_MAX_NODE_VOLUMES) + { + strlcpy(current_node->volumes[vi].name, (yyvsp[(2) - (3)].str), + sizeof(current_node->volumes[0].name)); + strlcpy(current_node->volumes[vi].path, (yyvsp[(3) - (3)].str), + sizeof(current_node->volumes[0].path)); + current_node->volumeCount++; + } + free((yyvsp[(2) - (3)].str)); + free((yyvsp[(3) - (3)].str)); + } + break; + + case 79: +#line 614 "test_spec_parse.y" + { + /* volume "/path/with spaces" */ + int vi = current_node->volumeCount; + if (vi < PGAF_MAX_NODE_VOLUMES) + { + strlcpy(current_node->volumes[vi].name, (yyvsp[(2) - (3)].str), + sizeof(current_node->volumes[0].name)); + strlcpy(current_node->volumes[vi].path, (yyvsp[(3) - (3)].str), + sizeof(current_node->volumes[0].path)); + current_node->volumeCount++; + } + free((yyvsp[(2) - (3)].str)); + free((yyvsp[(3) - (3)].str)); + } + break; + + case 80: +#line 635 "test_spec_parse.y" + { + current_spec->setup = (yyvsp[(2) - (2)].step); + } + break; + + case 81: +#line 642 "test_spec_parse.y" + { + current_spec->teardown = (yyvsp[(2) - (2)].step); + } + break; + + case 82: +#line 653 "test_spec_parse.y" + { + TestStep *s = (yyvsp[(3) - (3)].step); + strncpy(s->name, (yyvsp[(2) - (3)].str), sizeof(s->name) - 1); + free((yyvsp[(2) - (3)].str)); + register_step(current_spec, s); + } + break; + + case 83: +#line 671 "test_spec_parse.y" + { + /* post-process: CMD_SQL immediately before CMD_EXPECT_ERROR */ + for (TestCmd *c = (yyvsp[(2) - (3)].step)->commands; c; c = c->next) + { + if (c->kind == CMD_SQL && c->next && + c->next->kind == CMD_EXPECT_ERROR) + { + c->allowError = true; + } + } + (yyval.step) = (yyvsp[(2) - (3)].step); + } + break; + + case 84: +#line 685 "test_spec_parse.y" + { + (yyval.step) = make_step(""); + } + break; + + case 85: +#line 689 "test_spec_parse.y" + { + if ((yyvsp[(2) - (2)].cmd)) + { + append_cmd((yyvsp[(1) - (2)].step), (yyvsp[(2) - (2)].cmd)); + } + (yyval.step) = (yyvsp[(1) - (2)].step); + } + break; + + case 86: +#line 696 "test_spec_parse.y" + { + (yyval.cmd) = (yyvsp[(1) - (1)].cmd); + } + break; + + case 87: +#line 697 "test_spec_parse.y" + { + (yyval.cmd) = (yyvsp[(1) - (1)].cmd); + } + break; + + case 88: +#line 698 "test_spec_parse.y" + { + (yyval.cmd) = (yyvsp[(1) - (1)].cmd); + } + break; + + case 89: +#line 699 "test_spec_parse.y" + { + (yyval.cmd) = (yyvsp[(1) - (1)].cmd); + } + break; + + case 90: +#line 700 "test_spec_parse.y" + { + (yyval.cmd) = (yyvsp[(1) - (1)].cmd); + } + break; + + case 91: +#line 701 "test_spec_parse.y" + { + (yyval.cmd) = (yyvsp[(1) - (1)].cmd); + } + break; + + case 92: +#line 702 "test_spec_parse.y" + { + (yyval.cmd) = (yyvsp[(1) - (1)].cmd); + } + break; + + case 93: +#line 703 "test_spec_parse.y" + { + (yyval.cmd) = (yyvsp[(1) - (1)].cmd); + } + break; + + case 94: +#line 704 "test_spec_parse.y" + { + (yyval.cmd) = (yyvsp[(1) - (1)].cmd); + } + break; + + case 95: +#line 705 "test_spec_parse.y" + { + (yyval.cmd) = (yyvsp[(1) - (1)].cmd); + } + break; + + case 96: +#line 706 "test_spec_parse.y" + { + (yyval.cmd) = (yyvsp[(1) - (1)].cmd); + } + break; + + case 97: +#line 707 "test_spec_parse.y" + { + (yyval.cmd) = (yyvsp[(1) - (1)].cmd); + } + break; + + case 98: +#line 708 "test_spec_parse.y" + { + (yyval.cmd) = (yyvsp[(1) - (1)].cmd); + } + break; + + case 99: +#line 723 "test_spec_parse.y" + { + (yyval.cmd) = make_cmd(CMD_EXEC); + strlcpy((yyval.cmd)->service, (yyvsp[(2) - (3)].str), + sizeof((yyval.cmd)->service)); + strlcpy((yyval.cmd)->args, (yyvsp[(3) - (3)].str), + sizeof((yyval.cmd)->args)); + free((yyvsp[(2) - (3)].str)); + free((yyvsp[(3) - (3)].str)); + } + break; + + case 100: +#line 730 "test_spec_parse.y" + { + (yyval.cmd) = make_cmd(CMD_EXEC); + strlcpy((yyval.cmd)->service, (yyvsp[(2) - (2)].str), + sizeof((yyval.cmd)->service)); + free((yyvsp[(2) - (2)].str)); + } + break; + + case 101: +#line 736 "test_spec_parse.y" + { + (yyval.cmd) = make_cmd(CMD_EXEC_FAILS); + strlcpy((yyval.cmd)->service, (yyvsp[(2) - (3)].str), + sizeof((yyval.cmd)->service)); + strlcpy((yyval.cmd)->args, (yyvsp[(3) - (3)].str), + sizeof((yyval.cmd)->args)); + free((yyvsp[(2) - (3)].str)); + free((yyvsp[(3) - (3)].str)); + } + break; + + case 102: +#line 743 "test_spec_parse.y" + { + (yyval.cmd) = make_cmd(CMD_EXEC_FAILS); + strlcpy((yyval.cmd)->service, (yyvsp[(2) - (2)].str), + sizeof((yyval.cmd)->service)); + free((yyvsp[(2) - (2)].str)); + } + break; + + case 103: +#line 749 "test_spec_parse.y" + { + /* "pg_autoctl perform failover --formation auth" + * EXEC_ARGS returns T_IDENT for first word, T_SHELL_ARGS for rest */ + (yyval.cmd) = make_cmd(CMD_PG_AUTOCTL); + sformat((yyval.cmd)->args, sizeof((yyval.cmd)->args), "%s %s", + (yyvsp[(2) - (3)].str), (yyvsp[(3) - (3)].str)); + free((yyvsp[(2) - (3)].str)); + free((yyvsp[(3) - (3)].str)); + } + break; + + case 104: +#line 757 "test_spec_parse.y" + { + (yyval.cmd) = make_cmd(CMD_PG_AUTOCTL); + strlcpy((yyval.cmd)->args, (yyvsp[(2) - (2)].str), + sizeof((yyval.cmd)->args)); + free((yyvsp[(2) - (2)].str)); + } + break; + + case 105: +#line 763 "test_spec_parse.y" + { + (yyval.cmd) = make_cmd(CMD_PG_AUTOCTL); + } + break; + + case 108: +#line 801 "test_spec_parse.y" + { + if (!current_wait_cmd) + { + current_wait_cmd = make_cmd(CMD_WAIT_MULTI); + } + int i = current_wait_cmd->waitStateCount; + if (i < PGAF_MAX_WAIT_STATES) + { + strlcpy(current_wait_cmd->waitNodes[i], (yyvsp[(1) - (4)].str), + sizeof(current_wait_cmd->waitNodes[0])); + strlcpy(current_wait_cmd->waitStates[i], (yyvsp[(4) - (4)].str), + sizeof(current_wait_cmd->waitStates[0])); + current_wait_cmd->waitStateCount++; + } + free((yyvsp[(1) - (4)].str)); + } + break; + + case 109: +#line 816 "test_spec_parse.y" + { + if (!current_wait_cmd) + { + current_wait_cmd = make_cmd(CMD_WAIT_MULTI); + } + int i = current_wait_cmd->waitStateCount; + if (i < PGAF_MAX_WAIT_STATES) + { + strlcpy(current_wait_cmd->waitNodes[i], (yyvsp[(1) - (4)].str), + sizeof(current_wait_cmd->waitNodes[0])); + strlcpy(current_wait_cmd->waitStates[i], (yyvsp[(4) - (4)].str), + sizeof(current_wait_cmd->waitStates[0])); + current_wait_cmd->waitStateCount++; + } + free((yyvsp[(1) - (4)].str)); + free((yyvsp[(4) - (4)].str)); + } + break; + + case 114: +#line 856 "test_spec_parse.y" + { + /* current_pass_cmd set by the enclosing wait_cmd rule */ + if (current_pass_cmd && + current_pass_cmd->passThroughCount < PGAF_MAX_WAIT_STATES) + { + strlcpy( + current_pass_cmd->passThroughStates[current_pass_cmd-> + passThroughCount++], + (yyvsp[(1) - (1)].str), + sizeof(current_pass_cmd->passThroughStates[0])); + } + } + break; + + case 115: +#line 864 "test_spec_parse.y" + { + if (current_pass_cmd && + current_pass_cmd->passThroughCount < PGAF_MAX_WAIT_STATES) + { + strlcpy( + current_pass_cmd->passThroughStates[current_pass_cmd-> + passThroughCount++], + (yyvsp[(1) - (1)].str), + sizeof(current_pass_cmd->passThroughStates[0])); + } + free((yyvsp[(1) - (1)].str)); + } + break; + + case 116: +#line 872 "test_spec_parse.y" + { + if (current_pass_cmd && + current_pass_cmd->passThroughCount < PGAF_MAX_WAIT_STATES) + { + strlcpy( + current_pass_cmd->passThroughStates[current_pass_cmd-> + passThroughCount++], + (yyvsp[(3) - (3)].str), + sizeof(current_pass_cmd->passThroughStates[0])); + } + } + break; + + case 117: +#line 879 "test_spec_parse.y" + { + if (current_pass_cmd && + current_pass_cmd->passThroughCount < PGAF_MAX_WAIT_STATES) + { + strlcpy( + current_pass_cmd->passThroughStates[current_pass_cmd-> + passThroughCount++], + (yyvsp[(3) - (3)].str), + sizeof(current_pass_cmd->passThroughStates[0])); + } + free((yyvsp[(3) - (3)].str)); + } + break; + + case 118: +#line 890 "test_spec_parse.y" + { + current_pass_cmd = make_cmd(CMD_WAIT_STATE); + strlcpy(current_pass_cmd->service, (yyvsp[(3) - (6)].str), + sizeof(current_pass_cmd->service)); + strlcpy(current_pass_cmd->state, (yyvsp[(6) - (6)].str), + sizeof(current_pass_cmd->state)); + free((yyvsp[(3) - (6)].str)); + } + break; + + case 119: +#line 895 "test_spec_parse.y" + { + current_pass_cmd->timeoutSeconds = (yyvsp[(9) - (9)].ival); + (yyval.cmd) = current_pass_cmd; + current_pass_cmd = NULL; + } + break; + + case 120: +#line 901 "test_spec_parse.y" + { + current_pass_cmd = make_cmd(CMD_WAIT_STATE); + strlcpy(current_pass_cmd->service, (yyvsp[(3) - (6)].str), + sizeof(current_pass_cmd->service)); + strlcpy(current_pass_cmd->state, (yyvsp[(6) - (6)].str), + sizeof(current_pass_cmd->state)); + free((yyvsp[(3) - (6)].str)); + free((yyvsp[(6) - (6)].str)); + } + break; + + case 121: +#line 906 "test_spec_parse.y" + { + current_pass_cmd->timeoutSeconds = (yyvsp[(9) - (9)].ival); + (yyval.cmd) = current_pass_cmd; + current_pass_cmd = NULL; + } + break; + + case 122: +#line 912 "test_spec_parse.y" + { + (yyval.cmd) = make_cmd(CMD_WAIT_STATE); + (yyval.cmd)->kind = CMD_ASSERT_ASSIGNED; + strlcpy((yyval.cmd)->service, (yyvsp[(3) - (7)].str), + sizeof((yyval.cmd)->service)); + strlcpy((yyval.cmd)->state, (yyvsp[(6) - (7)].str), + sizeof((yyval.cmd)->state)); + (yyval.cmd)->timeoutSeconds = (yyvsp[(7) - (7)].ival); + free((yyvsp[(3) - (7)].str)); + } + break; + + case 123: +#line 921 "test_spec_parse.y" + { + (yyval.cmd) = make_cmd(CMD_WAIT_STATE); + (yyval.cmd)->kind = CMD_ASSERT_ASSIGNED; + strlcpy((yyval.cmd)->service, (yyvsp[(3) - (7)].str), + sizeof((yyval.cmd)->service)); + strlcpy((yyval.cmd)->state, (yyvsp[(6) - (7)].str), + sizeof((yyval.cmd)->state)); + (yyval.cmd)->timeoutSeconds = (yyvsp[(7) - (7)].ival); + free((yyvsp[(3) - (7)].str)); + free((yyvsp[(6) - (7)].str)); + } + break; + + case 124: +#line 930 "test_spec_parse.y" + { + (yyval.cmd) = make_cmd(CMD_WAIT_STOPPED); + strlcpy((yyval.cmd)->service, (yyvsp[(3) - (5)].str), + sizeof((yyval.cmd)->service)); + (yyval.cmd)->timeoutSeconds = (yyvsp[(5) - (5)].ival); + free((yyvsp[(3) - (5)].str)); + } + break; + + case 125: +#line 937 "test_spec_parse.y" + { + (yyval.cmd) = current_wait_cmd; + (yyval.cmd)->timeoutSeconds = (yyvsp[(5) - (5)].ival); + current_wait_cmd = NULL; + } + break; + + case 126: +#line 951 "test_spec_parse.y" + { + (yyval.cmd) = current_wait_cmd; + (yyval.cmd)->timeoutSeconds = (yyvsp[(6) - (6)].ival); + current_wait_cmd = NULL; + } + break; + + case 127: +#line 966 "test_spec_parse.y" + { + current_wait_cmd = make_cmd(CMD_WAIT_STATES); + strlcpy(current_wait_cmd->waitStates[current_wait_cmd->waitStateCount++], + (yyvsp[(1) - (1)].str), sizeof(current_wait_cmd->waitStates[0])); + } + break; + + case 128: +#line 972 "test_spec_parse.y" + { + current_wait_cmd = make_cmd(CMD_WAIT_STATES); + strlcpy(current_wait_cmd->waitStates[current_wait_cmd->waitStateCount++], + (yyvsp[(1) - (1)].str), sizeof(current_wait_cmd->waitStates[0])); + free((yyvsp[(1) - (1)].str)); + } + break; + + case 129: +#line 979 "test_spec_parse.y" + { + if (current_wait_cmd->waitStateCount < PGAF_MAX_WAIT_STATES) + { + strlcpy( + current_wait_cmd->waitStates[current_wait_cmd->waitStateCount++], + (yyvsp[(3) - (3)].str), + sizeof(current_wait_cmd->waitStates[0])); + } + } + break; + + case 130: +#line 985 "test_spec_parse.y" + { + if (current_wait_cmd->waitStateCount < PGAF_MAX_WAIT_STATES) + { + strlcpy( + current_wait_cmd->waitStates[current_wait_cmd->waitStateCount++], + (yyvsp[(3) - (3)].str), + sizeof(current_wait_cmd->waitStates[0])); + } + free((yyvsp[(3) - (3)].str)); + } + break; + + case 133: +#line 1004 "test_spec_parse.y" + { + if (current_wait_cmd->waitGroupCount < PGAF_MAX_WAIT_GROUPS) + { + current_wait_cmd->waitGroups[current_wait_cmd->waitGroupCount++] = + (yyvsp[(2) - (2)].ival); + } + } + break; + + case 134: +#line 1009 "test_spec_parse.y" + { + if (current_wait_cmd->waitGroupCount < PGAF_MAX_WAIT_GROUPS) + { + current_wait_cmd->waitGroups[current_wait_cmd->waitGroupCount++] = + (yyvsp[(4) - (4)].ival); + } + } + break; + + case 135: +#line 1016 "test_spec_parse.y" + { + (yyval.ival) = PGAF_TIMEOUT_DEFAULT; + } + break; + + case 136: +#line 1017 "test_spec_parse.y" + { + (yyval.ival) = (yyvsp[(2) - (2)].ival); + } + break; + + case 137: +#line 1018 "test_spec_parse.y" + { + (yyval.ival) = (yyvsp[(3) - (3)].ival); + } + break; + + case 138: +#line 1030 "test_spec_parse.y" + { + (yyval.cmd) = make_cmd((yyvsp[(6) - (6)].ival) > 0 ? CMD_WAIT_STATE : + CMD_ASSERT_STATE); + strlcpy((yyval.cmd)->service, (yyvsp[(2) - (6)].str), + sizeof((yyval.cmd)->service)); + strlcpy((yyval.cmd)->state, (yyvsp[(5) - (6)].str), + sizeof((yyval.cmd)->state)); + (yyval.cmd)->timeoutSeconds = (yyvsp[(6) - (6)].ival); + free((yyvsp[(2) - (6)].str)); + } + break; + + case 139: +#line 1038 "test_spec_parse.y" + { + (yyval.cmd) = make_cmd((yyvsp[(6) - (6)].ival) > 0 ? CMD_WAIT_STATE : + CMD_ASSERT_STATE); + strlcpy((yyval.cmd)->service, (yyvsp[(2) - (6)].str), + sizeof((yyval.cmd)->service)); + strlcpy((yyval.cmd)->state, (yyvsp[(5) - (6)].str), + sizeof((yyval.cmd)->state)); + (yyval.cmd)->timeoutSeconds = (yyvsp[(6) - (6)].ival); + free((yyvsp[(2) - (6)].str)); + free((yyvsp[(5) - (6)].str)); + } + break; + + case 140: +#line 1046 "test_spec_parse.y" + { + (yyval.cmd) = make_cmd(CMD_ASSERT_ASSIGNED); + strlcpy((yyval.cmd)->service, (yyvsp[(2) - (6)].str), + sizeof((yyval.cmd)->service)); + strlcpy((yyval.cmd)->state, (yyvsp[(5) - (6)].str), + sizeof((yyval.cmd)->state)); + (yyval.cmd)->timeoutSeconds = (yyvsp[(6) - (6)].ival); + free((yyvsp[(2) - (6)].str)); + } + break; + + case 141: +#line 1054 "test_spec_parse.y" + { + (yyval.cmd) = make_cmd(CMD_ASSERT_ASSIGNED); + strlcpy((yyval.cmd)->service, (yyvsp[(2) - (6)].str), + sizeof((yyval.cmd)->service)); + strlcpy((yyval.cmd)->state, (yyvsp[(5) - (6)].str), + sizeof((yyval.cmd)->state)); + (yyval.cmd)->timeoutSeconds = (yyvsp[(6) - (6)].ival); + free((yyvsp[(2) - (6)].str)); + free((yyvsp[(5) - (6)].str)); + } + break; + + case 142: +#line 1072 "test_spec_parse.y" + { + (yyval.cmd) = make_cmd(CMD_SQL); + strlcpy((yyval.cmd)->service, (yyvsp[(2) - (3)].str), + sizeof((yyval.cmd)->service)); + strlcpy((yyval.cmd)->args, (yyvsp[(3) - (3)].str), + sizeof((yyval.cmd)->args)); + free((yyvsp[(2) - (3)].str)); + free((yyvsp[(3) - (3)].str)); + } + break; + + case 143: +#line 1087 "test_spec_parse.y" + { + (yyval.cmd) = make_cmd(CMD_EXPECT); + strlcpy((yyval.cmd)->expected, (yyvsp[(2) - (2)].str), + sizeof((yyval.cmd)->expected)); + expand_tuple_expect((yyval.cmd)->expected, sizeof((yyval.cmd)->expected)); + free((yyvsp[(2) - (2)].str)); + } + break; + + case 144: +#line 1094 "test_spec_parse.y" + { + (yyval.cmd) = make_cmd(CMD_EXPECT_ERROR); + } + break; + + case 145: +#line 1098 "test_spec_parse.y" + { + (yyval.cmd) = make_cmd(CMD_EXPECT_ERROR); + strlcpy((yyval.cmd)->state, (yyvsp[(3) - (3)].str), + sizeof((yyval.cmd)->state)); + free((yyvsp[(3) - (3)].str)); + } + break; + + case 146: +#line 1104 "test_spec_parse.y" + { + /* SQLSTATE codes like 25006 are all digits, lexed as T_INTEGER */ + (yyval.cmd) = make_cmd(CMD_EXPECT_ERROR); + snprintf((yyval.cmd)->state, sizeof((yyval.cmd)->state), "%d", + (yyvsp[(3) - (3)].ival)); + } + break; + + case 147: +#line 1117 "test_spec_parse.y" + { + (yyval.cmd) = current_promote_cmd; + current_promote_cmd = NULL; + } + break; + + case 148: +#line 1125 "test_spec_parse.y" + { + current_promote_cmd = make_cmd(CMD_PROMOTE); + current_promote_cmd->timeoutSeconds = PGAF_TIMEOUT_DEFAULT; + strlcpy( + current_promote_cmd->promoteNodes[current_promote_cmd->promoteCount++], + (yyvsp[(1) - (1)].str), + sizeof(current_promote_cmd->promoteNodes[0])); + free((yyvsp[(1) - (1)].str)); + } + break; + + case 149: +#line 1133 "test_spec_parse.y" + { + if (current_promote_cmd->promoteCount < PGAF_MAX_PROMOTE_NODES) + { + strlcpy( + current_promote_cmd->promoteNodes[current_promote_cmd-> + promoteCount++], + (yyvsp[(3) - (3)].str), + sizeof(current_promote_cmd->promoteNodes[0])); + } + free((yyvsp[(3) - (3)].str)); + } + break; + + case 150: +#line 1148 "test_spec_parse.y" + { + (yyval.cmd) = make_cmd(CMD_NETWORK_OFF); + strlcpy((yyval.cmd)->service, (yyvsp[(3) - (3)].str), + sizeof((yyval.cmd)->service)); + free((yyvsp[(3) - (3)].str)); + } + break; + + case 151: +#line 1154 "test_spec_parse.y" + { + (yyval.cmd) = make_cmd(CMD_NETWORK_ON); + strlcpy((yyval.cmd)->service, (yyvsp[(3) - (3)].str), + sizeof((yyval.cmd)->service)); + free((yyvsp[(3) - (3)].str)); + } + break; + + case 152: +#line 1167 "test_spec_parse.y" + { + (yyval.cmd) = make_cmd(CMD_SLEEP); + (yyval.cmd)->timeoutSeconds = (yyvsp[(2) - (2)].ival); + } + break; + + case 153: +#line 1181 "test_spec_parse.y" + { + (yyval.cmd) = make_cmd(CMD_COMPOSE_DOWN); + } + break; + + case 154: +#line 1185 "test_spec_parse.y" + { + (yyval.cmd) = make_cmd(CMD_COMPOSE_START); + strlcpy((yyval.cmd)->service, (yyvsp[(3) - (3)].str), + sizeof((yyval.cmd)->service)); + free((yyvsp[(3) - (3)].str)); + } + break; + + case 155: +#line 1191 "test_spec_parse.y" + { + (yyval.cmd) = make_cmd(CMD_COMPOSE_STOP); + strlcpy((yyval.cmd)->service, (yyvsp[(3) - (3)].str), + sizeof((yyval.cmd)->service)); + free((yyvsp[(3) - (3)].str)); + } + break; + + case 156: +#line 1197 "test_spec_parse.y" + { + (yyval.cmd) = make_cmd(CMD_COMPOSE_KILL); + strlcpy((yyval.cmd)->service, (yyvsp[(3) - (3)].str), + sizeof((yyval.cmd)->service)); + free((yyvsp[(3) - (3)].str)); + } + break; + + case 157: +#line 1223 "test_spec_parse.y" + { + (yyval.cmd) = make_cmd(CMD_COMPOSE_INJECT); + strlcpy((yyval.cmd)->expected, (yyvsp[(3) - (4)].str), + sizeof((yyval.cmd)->expected)); /* image */ + + /* Split T_SHELL_ARGS: " :" */ + char tmp[4096]; + strlcpy(tmp, (yyvsp[(4) - (4)].str), sizeof(tmp)); + char *src = tmp; + char *p = tmp; + while (*p && *p != ' ' && *p != '\t') + { + p++; + } + if (*p) + { + *p++ = '\0'; + while (*p == ' ' || *p == '\t') + { + p++; + } + } + char *svcdst = p; + char *colon = (*svcdst) ? strchr(svcdst, ':') : NULL; + strlcpy((yyval.cmd)->args, src, sizeof((yyval.cmd)->args)); + if (colon) + { + *colon = '\0'; + strlcpy((yyval.cmd)->service, svcdst, sizeof((yyval.cmd)->service)); /* dst svc */ + strlcpy((yyval.cmd)->state, colon + 1, sizeof((yyval.cmd)->state)); /* dst path */ + } + free((yyvsp[(3) - (4)].str)); + free((yyvsp[(4) - (4)].str)); + } + break; + + case 158: +#line 1257 "test_spec_parse.y" + { + (yyval.cmd) = make_cmd(CMD_STOP_POSTGRES); + strlcpy((yyval.cmd)->service, (yyvsp[(3) - (3)].str), + sizeof((yyval.cmd)->service)); + free((yyvsp[(3) - (3)].str)); + } + break; + + case 159: +#line 1263 "test_spec_parse.y" + { + (yyval.cmd) = make_cmd(CMD_START_POSTGRES); + strlcpy((yyval.cmd)->service, (yyvsp[(3) - (3)].str), + sizeof((yyval.cmd)->service)); + free((yyvsp[(3) - (3)].str)); + } + break; + + case 160: +#line 1279 "test_spec_parse.y" + { + pgaf_next_brace_is_while = 1; + } + break; + + case 161: +#line 1280 "test_spec_parse.y" + { + (yyval.step) = (yyvsp[(4) - (5)].step); + } + break; + + case 162: +#line 1285 "test_spec_parse.y" + { + (yyval.cmd) = make_cmd(CMD_STAYS_WHILE); + strlcpy((yyval.cmd)->service, (yyvsp[(2) - (5)].str), + sizeof((yyval.cmd)->service)); + strlcpy((yyval.cmd)->state, (yyvsp[(4) - (5)].str), + sizeof((yyval.cmd)->state)); + (yyval.cmd)->body = ((yyvsp[(5) - (5)].step)) ? (yyvsp[(5) - + (5)].step)-> + commands : NULL; + free((yyvsp[(2) - (5)].str)); + } + break; + + case 163: +#line 1304 "test_spec_parse.y" + { + /* only "set monitor " is supported; $2 must be "monitor" */ + if (strcmp((yyvsp[(2) - (3)].str), "monitor") != 0) + { + fprintf(stderr, + "pgaftest: unknown 'set' target '%s' (expected 'monitor')\n", + (yyvsp[(2) - + ( + 3)].str)); + free((yyvsp[(2) - (3)].str)); + free((yyvsp[(3) - (3)].str)); + YYERROR; + } + (yyval.cmd) = make_cmd(CMD_SET_MONITOR); + strlcpy((yyval.cmd)->service, (yyvsp[(3) - (3)].str), + sizeof((yyval.cmd)->service)); + free((yyvsp[(2) - (3)].str)); + free((yyvsp[(3) - (3)].str)); + } + break; + + case 164: +#line 1329 "test_spec_parse.y" + { + (yyval.cmd) = make_cmd(CMD_LOGS_CHECK); + strlcpy((yyval.cmd)->service, (yyvsp[(2) - (4)].str), + sizeof((yyval.cmd)->service)); + strlcpy((yyval.cmd)->args, (yyvsp[(4) - (4)].str), + sizeof((yyval.cmd)->args)); + (yyval.cmd)->logsNegate = false; + (yyval.cmd)->allowError = false; /* false = fixed string, true = PCRE */ + free((yyvsp[(2) - (4)].str)); + free((yyvsp[(4) - (4)].str)); + } + break; + + case 165: +#line 1338 "test_spec_parse.y" + { + (yyval.cmd) = make_cmd(CMD_LOGS_CHECK); + strlcpy((yyval.cmd)->service, (yyvsp[(2) - (5)].str), + sizeof((yyval.cmd)->service)); + strlcpy((yyval.cmd)->args, (yyvsp[(5) - (5)].str), + sizeof((yyval.cmd)->args)); + (yyval.cmd)->logsNegate = true; + (yyval.cmd)->allowError = false; + free((yyvsp[(2) - (5)].str)); + free((yyvsp[(5) - (5)].str)); + } + break; + + case 166: +#line 1347 "test_spec_parse.y" + { + (yyval.cmd) = make_cmd(CMD_LOGS_CHECK); + strlcpy((yyval.cmd)->service, (yyvsp[(2) - (4)].str), + sizeof((yyval.cmd)->service)); + strlcpy((yyval.cmd)->args, (yyvsp[(4) - (4)].str), + sizeof((yyval.cmd)->args)); + (yyval.cmd)->logsNegate = false; + (yyval.cmd)->allowError = true; /* true = PCRE (-P) */ + free((yyvsp[(2) - (4)].str)); + free((yyvsp[(4) - (4)].str)); + } + break; + + case 167: +#line 1356 "test_spec_parse.y" + { + (yyval.cmd) = make_cmd(CMD_LOGS_CHECK); + strlcpy((yyval.cmd)->service, (yyvsp[(2) - (5)].str), + sizeof((yyval.cmd)->service)); + strlcpy((yyval.cmd)->args, (yyvsp[(5) - (5)].str), + sizeof((yyval.cmd)->args)); + (yyval.cmd)->logsNegate = true; + (yyval.cmd)->allowError = true; + free((yyvsp[(2) - (5)].str)); + free((yyvsp[(5) - (5)].str)); + } + break; + + case 170: +#line 1377 "test_spec_parse.y" + { + int i = current_spec->sequenceLength; + if (i < PGAF_MAX_SEQ) + { + current_spec->sequence[current_spec->sequenceLength++] = (yyvsp[(2) - + (2)]. + str); + } + else + { + fprintf(stderr, "pgaftest: too many steps in sequence (max %d)\n", + PGAF_MAX_SEQ); + exit(1); + } + } + break; + + case 171: +#line 1398 "test_spec_parse.y" + { + (yyval.str) = "init"; + } + break; + + case 172: +#line 1399 "test_spec_parse.y" + { + (yyval.str) = "single"; + } + break; + + case 173: +#line 1400 "test_spec_parse.y" + { + (yyval.str) = "primary"; + } + break; + + case 174: +#line 1401 "test_spec_parse.y" + { + (yyval.str) = "wait_primary"; + } + break; + + case 175: +#line 1402 "test_spec_parse.y" + { + (yyval.str) = "wait_standby"; + } + break; + + case 176: +#line 1403 "test_spec_parse.y" + { + (yyval.str) = "demoted"; + } + break; + + case 177: +#line 1404 "test_spec_parse.y" + { + (yyval.str) = "demote_timeout"; + } + break; + + case 178: +#line 1405 "test_spec_parse.y" + { + (yyval.str) = "draining"; + } + break; + + case 179: +#line 1406 "test_spec_parse.y" + { + (yyval.str) = "secondary"; + } + break; + + case 180: +#line 1407 "test_spec_parse.y" + { + (yyval.str) = "catchingup"; + } + break; + + case 181: +#line 1408 "test_spec_parse.y" + { + (yyval.str) = "prepare_promotion"; + } + break; + + case 182: +#line 1409 "test_spec_parse.y" + { + (yyval.str) = "stop_replication"; + } + break; + + case 183: +#line 1410 "test_spec_parse.y" + { + (yyval.str) = "maintenance"; + } + break; + + case 184: +#line 1411 "test_spec_parse.y" + { + (yyval.str) = "join_primary"; + } + break; + + case 185: +#line 1412 "test_spec_parse.y" + { + (yyval.str) = "apply_settings"; + } + break; + + case 186: +#line 1413 "test_spec_parse.y" + { + (yyval.str) = "prepare_maintenance"; + } + break; + + case 187: +#line 1414 "test_spec_parse.y" + { + (yyval.str) = "wait_maintenance"; + } + break; + + case 188: +#line 1415 "test_spec_parse.y" + { + (yyval.str) = "report_lsn"; + } + break; + + case 189: +#line 1416 "test_spec_parse.y" + { + (yyval.str) = "fast_forward"; + } + break; + + case 190: +#line 1417 "test_spec_parse.y" + { + (yyval.str) = "join_secondary"; + } + break; + + case 191: +#line 1418 "test_spec_parse.y" + { + (yyval.str) = "dropped"; + } + break; + + case 192: +#line 1426 "test_spec_parse.y" + { + (yyval.str) = (yyvsp[(1) - (1)].str); + } + break; + + case 193: +#line 1427 "test_spec_parse.y" + { + (yyval.str) = (yyvsp[(1) - (1)].str); + } + break; + + +/* Line 1267 of yacc.c. */ +#line 3360 "test_spec_parse.c" + default: + { } + break; + } + YY_SYMBOL_PRINT("-> $$ =", yyr1[yyn], &yyval, &yyloc); + + YYPOPSTACK(yylen); + yylen = 0; + YY_STACK_PRINT(yyss, yyssp); + + *++yyvsp = yyval; + + + /* Now `shift' the result of the reduction. Determine what state + * that goes to, based on the state we popped back to and the rule + * number reduced by. */ + + yyn = yyr1[yyn]; + + yystate = yypgoto[yyn - YYNTOKENS] + *yyssp; + if (0 <= yystate && yystate <= YYLAST && yycheck[yystate] == *yyssp) + { + yystate = yytable[yystate]; + } + else + { + yystate = yydefgoto[yyn - YYNTOKENS]; + } + + goto yynewstate; + + +/*------------------------------------. + | yyerrlab -- here on detecting error | + | `------------------------------------*/ +yyerrlab: + + /* If not already recovering from an error, report this error. */ + if (!yyerrstatus) + { + ++yynerrs; +#if !YYERROR_VERBOSE + yyerror(YY_("syntax error")); +#else + { + YYSIZE_T yysize = yysyntax_error(0, yystate, yychar); + if (yymsg_alloc < yysize && yymsg_alloc < YYSTACK_ALLOC_MAXIMUM) + { + YYSIZE_T yyalloc = 2 * yysize; + if (!(yysize <= yyalloc && yyalloc <= YYSTACK_ALLOC_MAXIMUM)) + { + yyalloc = YYSTACK_ALLOC_MAXIMUM; + } + if (yymsg != yymsgbuf) + { + YYSTACK_FREE(yymsg); + } + yymsg = (char *) YYSTACK_ALLOC(yyalloc); + if (yymsg) + { + yymsg_alloc = yyalloc; + } + else + { + yymsg = yymsgbuf; + yymsg_alloc = sizeof yymsgbuf; + } + } + + if (0 < yysize && yysize <= yymsg_alloc) + { + (void) yysyntax_error(yymsg, yystate, yychar); + yyerror(yymsg); + } + else + { + yyerror(YY_("syntax error")); + if (yysize != 0) + { + goto yyexhaustedlab; + } + } + } +#endif + } + + + if (yyerrstatus == 3) + { + /* If just tried and failed to reuse look-ahead token after an + * error, discard it. */ + + if (yychar <= YYEOF) + { + /* Return failure if at end of input. */ + if (yychar == YYEOF) + { + YYABORT; + } + } + else + { + yydestruct("Error: discarding", + yytoken, &yylval); + yychar = YYEMPTY; + } + } + + /* Else will try to reuse look-ahead token after shifting the error + * token. */ + goto yyerrlab1; + + +/*---------------------------------------------------. + | yyerrorlab -- error raised explicitly by YYERROR. | + | `---------------------------------------------------*/ +yyerrorlab: + + /* Pacify compilers like GCC when the user code never invokes + * YYERROR and the label yyerrorlab therefore never appears in user + * code. */ + if (/*CONSTCOND*/ 0) + { + goto yyerrorlab; + } + + /* Do not reclaim the symbols of the rule which action triggered + * this YYERROR. */ + YYPOPSTACK(yylen); + yylen = 0; + YY_STACK_PRINT(yyss, yyssp); + yystate = *yyssp; + goto yyerrlab1; + + +/*-------------------------------------------------------------. + | yyerrlab1 -- common code for both syntax error and YYERROR. | + | `-------------------------------------------------------------*/ +yyerrlab1: + yyerrstatus = 3; /* Each real token shifted decrements this. */ + + for (;;) + { + yyn = yypact[yystate]; + if (yyn != YYPACT_NINF) + { + yyn += YYTERROR; + if (0 <= yyn && yyn <= YYLAST && yycheck[yyn] == YYTERROR) + { + yyn = yytable[yyn]; + if (0 < yyn) + { + break; + } + } + } + + /* Pop the current state because it cannot handle the error token. */ + if (yyssp == yyss) + { + YYABORT; + } + + + yydestruct("Error: popping", + yystos[yystate], yyvsp); + YYPOPSTACK(1); + yystate = *yyssp; + YY_STACK_PRINT(yyss, yyssp); + } + + if (yyn == YYFINAL) + { + YYACCEPT; + } + + *++yyvsp = yylval; + + + /* Shift the error token. */ + YY_SYMBOL_PRINT("Shifting", yystos[yyn], yyvsp, yylsp); + + yystate = yyn; + goto yynewstate; + + +/*-------------------------------------. + | yyacceptlab -- YYACCEPT comes here. | + | `-------------------------------------*/ +yyacceptlab: + yyresult = 0; + goto yyreturn; + +/*-----------------------------------. + | yyabortlab -- YYABORT comes here. | + | `-----------------------------------*/ +yyabortlab: + yyresult = 1; + goto yyreturn; + +#ifndef yyoverflow + +/*-------------------------------------------------. + | yyexhaustedlab -- memory exhaustion comes here. | + | `-------------------------------------------------*/ +yyexhaustedlab: + yyerror(YY_("memory exhausted")); + yyresult = 2; + + /* Fall through. */ +#endif + +yyreturn: + if (yychar != YYEOF && yychar != YYEMPTY) + { + yydestruct("Cleanup: discarding lookahead", + yytoken, &yylval); + } + + /* Do not reclaim the symbols of the rule which action triggered + * this YYABORT or YYACCEPT. */ + YYPOPSTACK(yylen); + YY_STACK_PRINT(yyss, yyssp); + while (yyssp != yyss) + { + yydestruct("Cleanup: popping", + yystos[*yyssp], yyvsp); + YYPOPSTACK(1); + } +#ifndef yyoverflow + if (yyss != yyssa) + { + YYSTACK_FREE(yyss); + } +#endif +#if YYERROR_VERBOSE + if (yymsg != yymsgbuf) + { + YYSTACK_FREE(yymsg); + } +#endif + + /* Make sure YYID is used. */ + return YYID(yyresult); +} + + +#line 1430 "test_spec_parse.y" + + +/* ----------------------------------------------------------------------- + * Public entry point + * ----------------------------------------------------------------------- */ +TestSpec * +parse_test_spec(const char *filename) +{ + FILE *f = fopen(filename, "r"); + if (!f) + { + fprintf(stderr, "pgaftest: cannot open spec file \"%s\": %s\n", + filename, strerror(errno)); + return NULL; + } + + TestSpec *spec = (TestSpec *) calloc(1, sizeof(TestSpec)); + if (!spec) + { + fprintf(stderr, "out of memory\n"); + exit(1); + } + + strncpy(spec->filename, filename, sizeof(spec->filename) - 1); + + current_spec = spec; + pgaf_line_number = 1; + yyin = f; + yyparse(); + fclose(f); + + return spec; +} + + +TestCmd * +make_cmd(TestCmdKind kind) +{ + TestCmd *c = (TestCmd *) calloc(1, sizeof(TestCmd)); + if (!c) + { + fprintf(stderr, "out of memory\n"); + exit(1); + } + c->kind = kind; + c->timeoutSeconds = PGAF_TIMEOUT_DEFAULT; + return c; +} + + +TestStep * +make_step(const char *name) +{ + TestStep *s = (TestStep *) calloc(1, sizeof(TestStep)); + if (!s) + { + fprintf(stderr, "out of memory\n"); + exit(1); + } + if (name) + { + strncpy(s->name, name, sizeof(s->name) - 1); + } + return s; +} + + +TestStep * +spec_find_step(TestSpec *spec, const char *name) +{ + for (TestStep *s = spec->steps; s; s = s->next) + { + if (strcmp(s->name, name) == 0) + { + return s; + } + } + return NULL; +} diff --git a/src/bin/pgaftest/test_spec_parse.h b/src/bin/pgaftest/test_spec_parse.h new file mode 100644 index 000000000..6f62d6019 --- /dev/null +++ b/src/bin/pgaftest/test_spec_parse.h @@ -0,0 +1,278 @@ +/* A Bison parser, made by GNU Bison 2.3. */ + +/* Skeleton interface for Bison's Yacc-like parsers in C + * + * Copyright (C) 1984, 1989, 1990, 2000, 2001, 2002, 2003, 2004, 2005, 2006 + * Free Software Foundation, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. */ + +/* As a special exception, you may create a larger work that contains + * part or all of the Bison parser skeleton and distribute that work + * under terms of your choice, so long as that work isn't itself a + * parser generator using the skeleton or a modified version thereof + * as a parser skeleton. Alternatively, if you modify or redistribute + * the parser skeleton itself, you may (at your option) remove this + * special exception, which will cause the skeleton and the resulting + * Bison output files to be licensed under the GNU General Public + * License without this special exception. + * + * This special exception was added by the Free Software Foundation in + * version 2.2 of Bison. */ + +/* Tokens. */ +#ifndef YYTOKENTYPE +# define YYTOKENTYPE + +/* Put the tokens into the symbol table, so that GDB and other debuggers + * know about them. */ +enum yytokentype +{ + T_CLUSTER = 258, + T_MONITOR = 259, + T_NODE = 260, + T_CITUS_COORDINATOR = 261, + T_CITUS_WORKER = 262, + T_SETUP = 263, + T_TEARDOWN = 264, + T_STEP = 265, + T_SEQUENCE = 266, + T_EQUALS = 267, + T_IMAGE = 268, + T_IMAGE_TARGET = 269, + T_SSL = 270, + T_AUTH = 271, + T_AUTH_METHOD = 272, + T_FORMATION = 273, + T_NUM_SYNC = 274, + T_COORDINATOR = 275, + T_WORKER = 276, + T_ASYNC = 277, + T_NO_MONITOR = 278, + T_LAUNCH = 279, + T_DEFERRED = 280, + T_IMMEDIATE = 281, + T_INITIALLY = 282, + T_VOLUME = 283, + T_LISTEN = 284, + T_CITUS_SECONDARY = 285, + T_CANDIDATE_PRIORITY = 286, + T_PORT = 287, + T_PASSWORD = 288, + T_MONITOR_PASSWORD = 289, + T_CITUS_CLUSTER_NAME = 290, + T_DEBIAN_CLUSTER = 291, + T_REPLICATION_QUORUM = 292, + T_REPLICATION_PASSWORD = 293, + T_EXTENSION_VERSION = 294, + T_BIND_SOURCE = 295, + T_FS_INIT = 296, + T_FS_SINGLE = 297, + T_FS_PRIMARY = 298, + T_FS_WAIT_PRIMARY = 299, + T_FS_WAIT_STANDBY = 300, + T_FS_DEMOTED = 301, + T_FS_DEMOTE_TIMEOUT = 302, + T_FS_DRAINING = 303, + T_FS_SECONDARY = 304, + T_FS_CATCHINGUP = 305, + T_FS_PREP_PROMOTION = 306, + T_FS_STOP_REPLICATION = 307, + T_FS_MAINTENANCE = 308, + T_FS_JOIN_PRIMARY = 309, + T_FS_APPLY_SETTINGS = 310, + T_FS_PREPARE_MAINTENANCE = 311, + T_FS_WAIT_MAINTENANCE = 312, + T_FS_REPORT_LSN = 313, + T_FS_FAST_FORWARD = 314, + T_FS_JOIN_SECONDARY = 315, + T_FS_DROPPED = 316, + T_EXEC = 317, + T_EXEC_FAILS = 318, + T_PG_AUTOCTL = 319, + T_WAIT = 320, + T_UNTIL = 321, + T_TIMEOUT = 322, + T_AND = 323, + T_IS = 324, + T_WITH = 325, + T_ASSERT = 326, + T_SQL = 327, + T_EXPECT = 328, + T_ERROR = 329, + T_PROMOTE = 330, + T_NETWORK = 331, + T_DISCONNECT = 332, + T_CONNECT = 333, + T_SLEEP = 334, + T_COMPOSE = 335, + T_DOWN = 336, + T_START = 337, + T_STOP = 338, + T_STOPPED = 339, + T_KILL = 340, + T_INJECT = 341, + T_STATE = 342, + T_ASSIGNED_STATE = 343, + T_IN = 344, + T_GROUP = 345, + T_LBRACE = 346, + T_RBRACE = 347, + T_COMMA = 348, + T_POSTGRES = 349, + T_STAYS = 350, + T_WHILE = 351, + T_THROUGH = 352, + T_SET = 353, + T_LOGS = 354, + T_NOT = 355, + T_CONTAINS = 356, + T_MATCHES = 357, + T_INTEGER = 358, + T_IDENT = 359, + T_STRING = 360, + T_BLOCK = 361, + T_SHELL_ARGS = 362 +}; +#endif + +/* Tokens. */ +#define T_CLUSTER 258 +#define T_MONITOR 259 +#define T_NODE 260 +#define T_CITUS_COORDINATOR 261 +#define T_CITUS_WORKER 262 +#define T_SETUP 263 +#define T_TEARDOWN 264 +#define T_STEP 265 +#define T_SEQUENCE 266 +#define T_EQUALS 267 +#define T_IMAGE 268 +#define T_IMAGE_TARGET 269 +#define T_SSL 270 +#define T_AUTH 271 +#define T_AUTH_METHOD 272 +#define T_FORMATION 273 +#define T_NUM_SYNC 274 +#define T_COORDINATOR 275 +#define T_WORKER 276 +#define T_ASYNC 277 +#define T_NO_MONITOR 278 +#define T_LAUNCH 279 +#define T_DEFERRED 280 +#define T_IMMEDIATE 281 +#define T_INITIALLY 282 +#define T_VOLUME 283 +#define T_LISTEN 284 +#define T_CITUS_SECONDARY 285 +#define T_CANDIDATE_PRIORITY 286 +#define T_PORT 287 +#define T_PASSWORD 288 +#define T_MONITOR_PASSWORD 289 +#define T_CITUS_CLUSTER_NAME 290 +#define T_DEBIAN_CLUSTER 291 +#define T_REPLICATION_QUORUM 292 +#define T_REPLICATION_PASSWORD 293 +#define T_EXTENSION_VERSION 294 +#define T_BIND_SOURCE 295 +#define T_FS_INIT 296 +#define T_FS_SINGLE 297 +#define T_FS_PRIMARY 298 +#define T_FS_WAIT_PRIMARY 299 +#define T_FS_WAIT_STANDBY 300 +#define T_FS_DEMOTED 301 +#define T_FS_DEMOTE_TIMEOUT 302 +#define T_FS_DRAINING 303 +#define T_FS_SECONDARY 304 +#define T_FS_CATCHINGUP 305 +#define T_FS_PREP_PROMOTION 306 +#define T_FS_STOP_REPLICATION 307 +#define T_FS_MAINTENANCE 308 +#define T_FS_JOIN_PRIMARY 309 +#define T_FS_APPLY_SETTINGS 310 +#define T_FS_PREPARE_MAINTENANCE 311 +#define T_FS_WAIT_MAINTENANCE 312 +#define T_FS_REPORT_LSN 313 +#define T_FS_FAST_FORWARD 314 +#define T_FS_JOIN_SECONDARY 315 +#define T_FS_DROPPED 316 +#define T_EXEC 317 +#define T_EXEC_FAILS 318 +#define T_PG_AUTOCTL 319 +#define T_WAIT 320 +#define T_UNTIL 321 +#define T_TIMEOUT 322 +#define T_AND 323 +#define T_IS 324 +#define T_WITH 325 +#define T_ASSERT 326 +#define T_SQL 327 +#define T_EXPECT 328 +#define T_ERROR 329 +#define T_PROMOTE 330 +#define T_NETWORK 331 +#define T_DISCONNECT 332 +#define T_CONNECT 333 +#define T_SLEEP 334 +#define T_COMPOSE 335 +#define T_DOWN 336 +#define T_START 337 +#define T_STOP 338 +#define T_STOPPED 339 +#define T_KILL 340 +#define T_INJECT 341 +#define T_STATE 342 +#define T_ASSIGNED_STATE 343 +#define T_IN 344 +#define T_GROUP 345 +#define T_LBRACE 346 +#define T_RBRACE 347 +#define T_COMMA 348 +#define T_POSTGRES 349 +#define T_STAYS 350 +#define T_WHILE 351 +#define T_THROUGH 352 +#define T_SET 353 +#define T_LOGS 354 +#define T_NOT 355 +#define T_CONTAINS 356 +#define T_MATCHES 357 +#define T_INTEGER 358 +#define T_IDENT 359 +#define T_STRING 360 +#define T_BLOCK 361 +#define T_SHELL_ARGS 362 + + +#if !defined YYSTYPE && !defined YYSTYPE_IS_DECLARED +typedef union YYSTYPE +#line 145 "test_spec_parse.y" +{ + int ival; + char *str; + TestStep *step; + TestCmd *cmd; +} + +/* Line 1529 of yacc.c. */ +#line 270 "test_spec_parse.h" +YYSTYPE; +# define yystype YYSTYPE /* obsolescent; will be withdrawn */ +# define YYSTYPE_IS_DECLARED 1 +# define YYSTYPE_IS_TRIVIAL 1 +#endif + +extern YYSTYPE yylval; diff --git a/src/bin/pgaftest/test_spec_parse.y b/src/bin/pgaftest/test_spec_parse.y new file mode 100644 index 000000000..9af5fc113 --- /dev/null +++ b/src/bin/pgaftest/test_spec_parse.y @@ -0,0 +1,1489 @@ +%{ +/* + * src/bin/pgaftest/test_spec_parse.y + * Bison grammar for .pgaf test specification files. + * + * The outer structure (cluster, setup, teardown, step, sequence) is + * described here as bison rules. Inside step/setup/teardown bodies + * the flex lexer enters the STEP_BODY exclusive state and returns + * individual tokens for every keyword, identifier, integer, and + * punctuation character — no more hand-written strstr/strtok parsing. + * + * The cluster { } block is now also fully parsed by this grammar. + * The flex lexer enters the CLUSTER_BODY exclusive state when it sees + * the opening '{' after "cluster", returning proper tokens for every + * keyword, value, and punctuation inside. + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the PostgreSQL License. + */ + +#include +#include +#include + +#include "test_spec.h" +#include "pgsetup.h" +#include "file_utils.h" + +/* provided by test_spec_scan.l */ +extern int yylex(void); +extern int pgaf_line_number; +extern FILE *yyin; +extern int pgaf_next_brace_is_while; /* set before T_LBRACE for while body */ + +/* the spec we're building */ +static TestSpec *current_spec = NULL; + +static void yyerror(const char *msg) +{ + fprintf(stderr, "pgaftest: parse error at line %d: %s\n", + pgaf_line_number, msg); + exit(1); +} + +/* helpers */ +static void append_cmd(TestStep *step, TestCmd *cmd) +{ + if (!step->commands) + { + step->commands = cmd; + } + else + { + TestCmd *c = step->commands; + while (c->next) c = c->next; + c->next = cmd; + } +} + +/* + * expand_tuple_expect — convert `{ r1 } { r2 }` tuple syntax into + * the newline-separated form that psql --tuples-only --no-align produces. + */ +static void +expand_tuple_expect(char *buf, int buflen) +{ + const char *p = buf; + while (*p == ' ' || *p == '\t') p++; + + if (p[0] != '{' || (p[1] != ' ' && p[1] != '\t')) + return; + + char tmp[4096] = { 0 }; + int pos = 0; + bool first = true; + + while (*p) + { + while (*p == ' ' || *p == '\t' || *p == '\n') p++; + if (*p == '\0') break; + if (*p != '{') break; + + p++; + while (*p == ' ' || *p == '\t') p++; + + char row[1024] = { 0 }; + int ri = 0; + int depth = 1; + while (*p && depth > 0) + { + if (*p == '{') depth++; + else if (*p == '}') { if (--depth == 0) break; } + if (depth > 0 && ri < (int)sizeof(row) - 1) + row[ri++] = *p; + p++; + } + if (*p == '}') p++; + + while (ri > 0 && (row[ri-1] == ' ' || row[ri-1] == '\t')) ri--; + row[ri] = '\0'; + + if (!first && pos < (int)sizeof(tmp) - 1) + tmp[pos++] = '\n'; + int l = ri; + if (pos + l >= (int)sizeof(tmp)) l = (int)sizeof(tmp) - pos - 1; + if (l > 0) { memcpy(tmp + pos, row, l); pos += l; } + tmp[pos] = '\0'; + first = false; + } + + if (!first) + strncpy(buf, tmp, buflen - 1); +} + +static void register_step(TestSpec *spec, TestStep *step) +{ + if (!spec->steps) + { + spec->steps = step; + } + else + { + TestStep *s = spec->steps; + while (s->next) s = s->next; + s->next = step; + } + spec->stepCount++; +} + +/* ----------------------------------------------------------------------- + * Static state used by multi-element grammar rules. + * + * The parser is single-threaded; these are only live during the reduction + * of a single rule so there is no re-entrancy concern. + * ----------------------------------------------------------------------- */ + +static TestCmd *current_wait_cmd = NULL; +static TestCmd *current_promote_cmd = NULL; +static TestCmd *current_pass_cmd = NULL; /* for opt_passing_through */ +static TestFormation *current_formation = NULL; +static TestNode *current_node = NULL; + +%} + +%union { + int ival; + char *str; + TestStep *step; + TestCmd *cmd; +} + +/* ---- Outer-structure tokens (used in INITIAL lex state) ---- */ +%token T_CLUSTER T_MONITOR T_NODE T_CITUS_COORDINATOR T_CITUS_WORKER +%token T_SETUP T_TEARDOWN T_STEP T_SEQUENCE +%token T_EQUALS + +/* ---- Cluster-body tokens ---- */ +%token T_IMAGE T_IMAGE_TARGET T_SSL T_AUTH T_AUTH_METHOD T_FORMATION T_NUM_SYNC +%token T_COORDINATOR T_WORKER T_ASYNC T_NO_MONITOR +%token T_LAUNCH T_DEFERRED T_IMMEDIATE T_INITIALLY T_VOLUME +%token T_LISTEN T_CITUS_SECONDARY T_CANDIDATE_PRIORITY T_PORT T_PASSWORD T_MONITOR_PASSWORD +%token T_CITUS_CLUSTER_NAME T_DEBIAN_CLUSTER T_REPLICATION_QUORUM T_REPLICATION_PASSWORD +%token T_EXTENSION_VERSION T_BIND_SOURCE + +/* ---- FSM state tokens (used in CLUSTER_BODY and STEP_BODY) ---- */ +%token T_FS_INIT T_FS_SINGLE T_FS_PRIMARY +%token T_FS_WAIT_PRIMARY T_FS_WAIT_STANDBY +%token T_FS_DEMOTED T_FS_DEMOTE_TIMEOUT T_FS_DRAINING +%token T_FS_SECONDARY T_FS_CATCHINGUP +%token T_FS_PREP_PROMOTION T_FS_STOP_REPLICATION +%token T_FS_MAINTENANCE T_FS_JOIN_PRIMARY T_FS_APPLY_SETTINGS +%token T_FS_PREPARE_MAINTENANCE T_FS_WAIT_MAINTENANCE +%token T_FS_REPORT_LSN T_FS_FAST_FORWARD T_FS_JOIN_SECONDARY +%token T_FS_DROPPED + +/* ---- Step-body tokens (used in STEP_BODY lex state) ---- */ +%token T_EXEC T_EXEC_FAILS T_PG_AUTOCTL +%token T_WAIT T_UNTIL T_TIMEOUT T_AND T_IS T_WITH +%token T_ASSERT +%token T_SQL T_EXPECT T_ERROR +%token T_PROMOTE +%token T_NETWORK T_DISCONNECT T_CONNECT +%token T_SLEEP +%token T_COMPOSE T_DOWN T_START T_STOP T_STOPPED T_KILL T_INJECT +%token T_STATE T_ASSIGNED_STATE +%token T_IN T_GROUP +%token T_LBRACE T_RBRACE T_COMMA +%token T_POSTGRES T_STAYS T_WHILE T_THROUGH T_SET +%token T_LOGS T_NOT T_CONTAINS T_MATCHES + +/* ---- Tokens with values ---- */ +%token T_INTEGER +%token T_IDENT T_STRING T_BLOCK T_SHELL_ARGS + +/* ---- Non-terminal types ---- */ +%type ident_or_string +%type bare_name +%type fsm_state +%type node_name +%type cmd_block cmd_list +%type step_cmd +%type exec_cmd wait_cmd assert_cmd sql_cmd expect_cmd +%type promote_cmd network_cmd sleep_cmd compose_cmd +%type postgres_ctl_cmd stays_while_cmd set_monitor_cmd logs_cmd +%type opt_timeout +%type while_body + +%% + +spec: + spec_item + | spec spec_item + ; + +spec_item: + cluster_block + | setup_block + | teardown_block + | named_step + | sequence_block + ; + +/* ----------------------------------------------------------------------- + * cluster { } + * + * The flex lexer enters CLUSTER_BODY on the opening '{' and returns to + * INITIAL on the outermost closing '}'. Every keyword and value inside + * the cluster block is a proper token — no hand-written string parsing. + * ----------------------------------------------------------------------- */ + +cluster_block: + T_CLUSTER T_LBRACE + { + strlcpy(current_spec->cluster.ssl, "self-signed", + sizeof(current_spec->cluster.ssl)); + strlcpy(current_spec->cluster.auth, "trust", + sizeof(current_spec->cluster.auth)); + } + cluster_item_list T_RBRACE + ; + +cluster_item_list: + /* empty */ + | cluster_item_list cluster_item + ; + +cluster_item: + monitor_line + | image_line + | ssl_line + | auth_line + | extension_version_line + | formation_block + | T_BIND_SOURCE { current_spec->cluster.bindSource = true; } + ; + +/* + * monitor [port N] + * + * The monitor keyword may optionally be followed by "port N". The ssl and + * auth settings for the monitor are handled by the top-level ssl_line and + * auth_line rules (they write to the same TestCluster fields), so we do not + * duplicate them here. Keeping only T_PORT avoids shift/reduce conflicts + * with ssl_line and auth_line in cluster_item_list. + */ +monitor_line: + T_MONITOR + { + current_spec->cluster.withMonitor = true; + } + | T_MONITOR T_DEBIAN_CLUSTER T_IDENT + { + current_spec->cluster.withMonitor = true; + strlcpy(current_spec->cluster.monitorDebianCluster, $3, + sizeof(current_spec->cluster.monitorDebianCluster)); + free($3); + } + | T_MONITOR T_IMAGE_TARGET T_IDENT + { + current_spec->cluster.withMonitor = true; + strlcpy(current_spec->cluster.monitorImageTarget, $3, + sizeof(current_spec->cluster.monitorImageTarget)); + free($3); + } + | T_MONITOR T_PORT T_INTEGER + { + current_spec->cluster.withMonitor = true; + /* monitor port not stored in TestCluster yet; ignore */ + (void) $3; + } + | T_MONITOR T_PASSWORD T_STRING + { + current_spec->cluster.withMonitor = true; + strlcpy(current_spec->cluster.monitorPassword, $3, + sizeof(current_spec->cluster.monitorPassword)); + free($3); + } + | T_MONITOR T_IDENT T_LAUNCH T_DEFERRED + { + strlcpy(current_spec->cluster.secondMonitorName, $2, + sizeof(current_spec->cluster.secondMonitorName)); + current_spec->cluster.secondMonitorStopped = true; + free($2); + } + | T_MONITOR T_IDENT T_INITIALLY T_STOPPED + { + strlcpy(current_spec->cluster.secondMonitorName, $2, + sizeof(current_spec->cluster.secondMonitorName)); + current_spec->cluster.secondMonitorStopped = true; + free($2); + } + | T_MONITOR T_IDENT T_LAUNCH T_DEFERRED T_PASSWORD T_STRING + { + strlcpy(current_spec->cluster.secondMonitorName, $2, + sizeof(current_spec->cluster.secondMonitorName)); + current_spec->cluster.secondMonitorStopped = true; + free($2); + /* password for second monitor not yet stored */ + free($6); + } + ; + +/* image "tag" | image tag */ +image_line: + T_IMAGE T_STRING + { + strlcpy(current_spec->cluster.image, $2, + sizeof(current_spec->cluster.image)); + free($2); + } + | T_IMAGE T_IDENT + { + strlcpy(current_spec->cluster.image, $2, + sizeof(current_spec->cluster.image)); + free($2); + } + ; + +/* extension-version VALUE — sets PG_AUTOCTL_EXTENSION_VERSION on the monitor */ +extension_version_line: + T_EXTENSION_VERSION T_IDENT + { + strlcpy(current_spec->cluster.extensionVersion, $2, + sizeof(current_spec->cluster.extensionVersion)); + free($2); + } + | T_EXTENSION_VERSION T_STRING + { + strlcpy(current_spec->cluster.extensionVersion, $2, + sizeof(current_spec->cluster.extensionVersion)); + free($2); + } + ; + +/* ssl VALUE */ +ssl_line: + T_SSL T_IDENT + { + strlcpy(current_spec->cluster.ssl, $2, + sizeof(current_spec->cluster.ssl)); + free($2); + } + ; + +/* auth VALUE | auth-method VALUE */ +auth_line: + T_AUTH T_IDENT + { + strlcpy(current_spec->cluster.auth, $2, + sizeof(current_spec->cluster.auth)); + free($2); + } + | T_AUTH_METHOD T_IDENT + { + strlcpy(current_spec->cluster.auth, $2, + sizeof(current_spec->cluster.auth)); + free($2); + } + ; + +/* formation [name] [num-sync N] { node_list } */ +formation_block: + T_FORMATION + { + TestCluster *cl = ¤t_spec->cluster; + if (cl->formationCount >= PGAF_MAX_FORMATIONS) + { + fprintf(stderr, "pgaftest: too many formations (max %d)\n", + PGAF_MAX_FORMATIONS); + exit(1); + } + current_formation = &cl->formations[cl->formationCount++]; + strlcpy(current_formation->name, "default", + sizeof(current_formation->name)); + current_formation->numSync = -1; + } + formation_opt_list T_LBRACE node_list T_RBRACE + ; + +formation_opt_list: + /* empty */ + | formation_opt_list formation_opt + ; + +/* + * bare_name allows any identifier or quoted string as a name, plus keywords + * that are likely to be used as formation/node names (e.g. "auth", "node", + * "monitor"). Using a keyword as a name is a common source of parse errors. + */ +bare_name: + T_IDENT { $$ = $1; } + | T_STRING { $$ = $1; } + | T_AUTH { $$ = strdup("auth"); } + | T_MONITOR { $$ = strdup("monitor"); } + | T_NODE { $$ = strdup("node"); } + ; + +formation_opt: + bare_name + { + strlcpy(current_formation->name, $1, sizeof(current_formation->name)); + free($1); + } + | T_NUM_SYNC T_INTEGER + { + current_formation->numSync = $2; + } + ; + +node_list: + /* empty */ + | node_list node_line + ; + +/* + * node_name — the first token on a node line. + * + * Node names are usually plain identifiers like "node1", "coord", "w1". + * They can also collide with reserved cluster keywords (e.g. a node named + * "monitor" or "node"). We allow T_MONITOR and T_NODE here so the grammar + * doesn't choke on such names. The FSM-state catch-all handles any state + * name used as a node name (unlikely but defensive). + */ +/* + * node_name covers bare-identifier node names used in the flat syntax. + * T_NODE is intentionally excluded: "node" is reserved for the block syntax + * "node foo { ... }" and must not be reduced here to avoid a shift-reduce + * conflict with T_NODE T_IDENT T_LBRACE. + */ +node_name: + T_IDENT { $$ = $1; } + | T_MONITOR { $$ = strdup("monitor"); } + ; + +/* + * init_node_slot — shared mid-rule action that allocates a node slot and + * sets defaults. Used by both node_line productions. + */ +init_node_slot: + /* empty */ + { + if (current_formation->nodeCount >= PGAF_MAX_NODES) + { + fprintf(stderr, "pgaftest: too many nodes in formation (max %d)\n", + PGAF_MAX_NODES); + exit(1); + } + current_node = ¤t_formation->nodes[current_formation->nodeCount++]; + current_node->kind = NODE_KIND_STANDALONE; + current_node->candidatePriority = 50; + current_node->replicationQuorum = true; + } + ; + +node_line: + /* flat syntax: node1 listen port 5432 ... */ + node_name init_node_slot + { + strlcpy(current_node->name, $1, sizeof(current_node->name)); + free($1); + } + node_opt_list + /* block syntax: node foo { listen \n port 5432 \n ... } */ + | T_NODE T_IDENT init_node_slot + { + strlcpy(current_node->name, $2, sizeof(current_node->name)); + free($2); + } + T_LBRACE node_opt_list T_RBRACE + ; + +node_opt_list: + /* empty */ + | node_opt_list node_opt + ; + +node_opt: + T_COORDINATOR + { + current_node->kind = NODE_KIND_CITUS_COORDINATOR; + current_spec->cluster.withCitus = true; + } + | T_WORKER + { + current_node->kind = NODE_KIND_CITUS_WORKER; + current_spec->cluster.withCitus = true; + } + | T_ASYNC + { + current_node->replicationQuorum = false; + } + | T_NO_MONITOR + { + current_node->noMonitor = true; + } + | T_DEFERRED + { + current_node->launchDeferred = true; + } + | T_LAUNCH T_DEFERRED + { + current_node->launchDeferred = true; + } + | T_LAUNCH T_IMMEDIATE + { + current_node->launchDeferred = false; + } + | T_IMMEDIATE + { + current_node->launchDeferred = false; + } + | T_LISTEN + { + current_node->listen = true; + } + | T_CITUS_SECONDARY + { + current_node->citusSecondary = true; + } + | T_CANDIDATE_PRIORITY T_INTEGER + { + current_node->candidatePriority = $2; + } + | T_GROUP T_INTEGER + { + current_node->group = $2; + } + | T_PORT T_INTEGER + { + current_node->pgPort = $2; + } + | T_CITUS_CLUSTER_NAME T_IDENT + { + strlcpy(current_node->citusClusterName, $2, + sizeof(current_node->citusClusterName)); + free($2); + } + | T_DEBIAN_CLUSTER T_IDENT + { + strlcpy(current_node->debianCluster, $2, + sizeof(current_node->debianCluster)); + free($2); + } + | T_SSL T_IDENT + { + strlcpy(current_node->ssl, $2, sizeof(current_node->ssl)); + free($2); + } + | T_AUTH T_IDENT + { + strlcpy(current_node->auth, $2, sizeof(current_node->auth)); + free($2); + } + | T_AUTH_METHOD T_IDENT + { + strlcpy(current_node->auth, $2, sizeof(current_node->auth)); + free($2); + } + | T_REPLICATION_QUORUM T_IDENT + { + if (strcmp($2, "false") == 0 || strcmp($2, "0") == 0) + current_node->replicationQuorum = false; + else + current_node->replicationQuorum = true; + free($2); + } + | T_REPLICATION_PASSWORD T_STRING + { + strlcpy(current_node->replicationPassword, $2, + sizeof(current_node->replicationPassword)); + free($2); + } + | T_MONITOR_PASSWORD T_STRING + { + strlcpy(current_node->monitorPassword, $2, + sizeof(current_node->monitorPassword)); + free($2); + } + | T_VOLUME T_IDENT T_IDENT + { + /* volume — adds a named Docker volume */ + int vi = current_node->volumeCount; + if (vi < PGAF_MAX_NODE_VOLUMES) + { + strlcpy(current_node->volumes[vi].name, $2, + sizeof(current_node->volumes[0].name)); + strlcpy(current_node->volumes[vi].path, $3, + sizeof(current_node->volumes[0].path)); + current_node->volumeCount++; + } + free($2); free($3); + } + | T_VOLUME T_IDENT T_STRING + { + /* volume "/path/with spaces" */ + int vi = current_node->volumeCount; + if (vi < PGAF_MAX_NODE_VOLUMES) + { + strlcpy(current_node->volumes[vi].name, $2, + sizeof(current_node->volumes[0].name)); + strlcpy(current_node->volumes[vi].path, $3, + sizeof(current_node->volumes[0].path)); + current_node->volumeCount++; + } + free($2); free($3); + } + ; + +/* ----------------------------------------------------------------------- + * setup { } and teardown { } + * ----------------------------------------------------------------------- */ + +setup_block: + T_SETUP cmd_block + { + current_spec->setup = $2; + } + ; + +teardown_block: + T_TEARDOWN cmd_block + { + current_spec->teardown = $2; + } + ; + +/* ----------------------------------------------------------------------- + * step { } + * ----------------------------------------------------------------------- */ + +named_step: + T_STEP ident_or_string cmd_block + { + TestStep *s = $3; + strncpy(s->name, $2, sizeof(s->name) - 1); + free($2); + register_step(current_spec, s); + } + ; + +/* ----------------------------------------------------------------------- + * Step body: T_LBRACE ... T_RBRACE + * + * The flex lexer enters STEP_BODY state when it sees the opening T_LBRACE + * and returns to INITIAL state after the closing T_RBRACE. Inside, every + * keyword and value is a distinct token — no strstr/strtok parsing. + * ----------------------------------------------------------------------- */ + +cmd_block: + T_LBRACE cmd_list T_RBRACE + { + /* post-process: CMD_SQL immediately before CMD_EXPECT_ERROR */ + for (TestCmd *c = $2->commands; c; c = c->next) + { + if (c->kind == CMD_SQL && c->next && + c->next->kind == CMD_EXPECT_ERROR) + c->allowError = true; + } + $$ = $2; + } + ; + +cmd_list: + /* empty */ + { + $$ = make_step(""); + } + | cmd_list step_cmd + { + if ($2) append_cmd($1, $2); + $$ = $1; + } + ; + +step_cmd: + exec_cmd { $$ = $1; } + | wait_cmd { $$ = $1; } + | assert_cmd { $$ = $1; } + | sql_cmd { $$ = $1; } + | expect_cmd { $$ = $1; } + | promote_cmd { $$ = $1; } + | network_cmd { $$ = $1; } + | sleep_cmd { $$ = $1; } + | compose_cmd { $$ = $1; } + | postgres_ctl_cmd { $$ = $1; } + | stays_while_cmd { $$ = $1; } + | set_monitor_cmd { $$ = $1; } + | logs_cmd { $$ = $1; } + ; + +/* ----------------------------------------------------------------------- + * exec + * exec-fails + * + * The flex EXEC_ARGS / EXEC_ARGS_REST states return: + * T_EXEC or T_EXEC_FAILS + * T_IDENT (the service name) + * T_SHELL_ARGS (rest of line, trimmed; omitted when line is empty) + * ----------------------------------------------------------------------- */ + +exec_cmd: + T_EXEC T_IDENT T_SHELL_ARGS + { + $$ = make_cmd(CMD_EXEC); + strlcpy($$->service, $2, sizeof($$->service)); + strlcpy($$->args, $3, sizeof($$->args)); + free($2); free($3); + } + | T_EXEC T_IDENT + { + $$ = make_cmd(CMD_EXEC); + strlcpy($$->service, $2, sizeof($$->service)); + free($2); + } + | T_EXEC_FAILS T_IDENT T_SHELL_ARGS + { + $$ = make_cmd(CMD_EXEC_FAILS); + strlcpy($$->service, $2, sizeof($$->service)); + strlcpy($$->args, $3, sizeof($$->args)); + free($2); free($3); + } + | T_EXEC_FAILS T_IDENT + { + $$ = make_cmd(CMD_EXEC_FAILS); + strlcpy($$->service, $2, sizeof($$->service)); + free($2); + } + | T_PG_AUTOCTL T_IDENT T_SHELL_ARGS + { + /* "pg_autoctl perform failover --formation auth" + * EXEC_ARGS returns T_IDENT for first word, T_SHELL_ARGS for rest */ + $$ = make_cmd(CMD_PG_AUTOCTL); + sformat($$->args, sizeof($$->args), "%s %s", $2, $3); + free($2); free($3); + } + | T_PG_AUTOCTL T_IDENT + { + $$ = make_cmd(CMD_PG_AUTOCTL); + strlcpy($$->args, $2, sizeof($$->args)); + free($2); + } + | T_PG_AUTOCTL + { + $$ = make_cmd(CMD_PG_AUTOCTL); + } + ; + +/* ----------------------------------------------------------------------- + * wait until ... + * + * Four forms, disambiguated cleanly by the token immediately after the + * first T_IDENT (one token of LALR lookahead is sufficient): + * + * wait until state = [timeout Ns] + * wait until assigned-state = [timeout Ns] + * wait until stopped [timeout Ns] + * wait until [, ...] [in group N[, group M]] [timeout Ns] + * + * FSM state names (T_FS_*) are accepted wherever a state string is expected. + * T_IDENT covers node names (e.g. "node1") and any future unknown states. + * ----------------------------------------------------------------------- */ + +/* + * state_op: accept '=' or 'is' as state comparison operator. + * This lets writers use either form: + * wait until node1 state = primary + * wait until node1 state is primary + */ +state_op: T_EQUALS | T_IS ; + +/* + * wait_multi_condition_list: one or more "node state [is|=] state" conditions + * joined by 'and'. Builds a CMD_WAIT_MULTI command via the current_wait_cmd + * global. + * + * wait until node2 state is primary and node1 state is secondary timeout 90s + * wait until node2 state is primary and node1 state is secondary with timeout 90s + */ +wait_multi_condition: + T_IDENT T_STATE state_op fsm_state + { + if (!current_wait_cmd) + current_wait_cmd = make_cmd(CMD_WAIT_MULTI); + int i = current_wait_cmd->waitStateCount; + if (i < PGAF_MAX_WAIT_STATES) + { + strlcpy(current_wait_cmd->waitNodes[i], $1, + sizeof(current_wait_cmd->waitNodes[0])); + strlcpy(current_wait_cmd->waitStates[i], $4, + sizeof(current_wait_cmd->waitStates[0])); + current_wait_cmd->waitStateCount++; + } + free($1); + } + | T_IDENT T_STATE state_op T_IDENT + { + if (!current_wait_cmd) + current_wait_cmd = make_cmd(CMD_WAIT_MULTI); + int i = current_wait_cmd->waitStateCount; + if (i < PGAF_MAX_WAIT_STATES) + { + strlcpy(current_wait_cmd->waitNodes[i], $1, + sizeof(current_wait_cmd->waitNodes[0])); + strlcpy(current_wait_cmd->waitStates[i], $4, + sizeof(current_wait_cmd->waitStates[0])); + current_wait_cmd->waitStateCount++; + } + free($1); free($4); + } + ; + +wait_multi_condition_list: + wait_multi_condition + | wait_multi_condition_list T_AND wait_multi_condition + ; + +/* + * opt_passing_through — optional "passing through s1, s2, ..." clause. + * + * These are intermediate FSM states that the node is expected to transit + * through on the way to the target state. The runner verifies them via + * LISTEN/NOTIFY on the monitor's state-change notifications. Missing any + * listed intermediate state is a test failure. + * + * Syntax (after the target state, before timeout): + * wait until node1 state is primary passing through wait_primary timeout 90s + */ + +opt_passing_through: + /* empty */ + | T_THROUGH pass_state_list + ; + +pass_state_list: + fsm_state + { + /* current_pass_cmd set by the enclosing wait_cmd rule */ + if (current_pass_cmd && + current_pass_cmd->passThroughCount < PGAF_MAX_WAIT_STATES) + strlcpy(current_pass_cmd->passThroughStates[current_pass_cmd->passThroughCount++], + $1, sizeof(current_pass_cmd->passThroughStates[0])); + } + | T_IDENT + { + if (current_pass_cmd && + current_pass_cmd->passThroughCount < PGAF_MAX_WAIT_STATES) + strlcpy(current_pass_cmd->passThroughStates[current_pass_cmd->passThroughCount++], + $1, sizeof(current_pass_cmd->passThroughStates[0])); + free($1); + } + | pass_state_list T_COMMA fsm_state + { + if (current_pass_cmd && + current_pass_cmd->passThroughCount < PGAF_MAX_WAIT_STATES) + strlcpy(current_pass_cmd->passThroughStates[current_pass_cmd->passThroughCount++], + $3, sizeof(current_pass_cmd->passThroughStates[0])); + } + | pass_state_list T_COMMA T_IDENT + { + if (current_pass_cmd && + current_pass_cmd->passThroughCount < PGAF_MAX_WAIT_STATES) + strlcpy(current_pass_cmd->passThroughStates[current_pass_cmd->passThroughCount++], + $3, sizeof(current_pass_cmd->passThroughStates[0])); + free($3); + } + ; + +wait_cmd: + T_WAIT T_UNTIL T_IDENT T_STATE state_op fsm_state + { current_pass_cmd = make_cmd(CMD_WAIT_STATE); + strlcpy(current_pass_cmd->service, $3, sizeof(current_pass_cmd->service)); + strlcpy(current_pass_cmd->state, $6, sizeof(current_pass_cmd->state)); + free($3); } + opt_passing_through opt_timeout + { + current_pass_cmd->timeoutSeconds = $9; + $$ = current_pass_cmd; + current_pass_cmd = NULL; + } + | T_WAIT T_UNTIL T_IDENT T_STATE state_op T_IDENT + { current_pass_cmd = make_cmd(CMD_WAIT_STATE); + strlcpy(current_pass_cmd->service, $3, sizeof(current_pass_cmd->service)); + strlcpy(current_pass_cmd->state, $6, sizeof(current_pass_cmd->state)); + free($3); free($6); } + opt_passing_through opt_timeout + { + current_pass_cmd->timeoutSeconds = $9; + $$ = current_pass_cmd; + current_pass_cmd = NULL; + } + | T_WAIT T_UNTIL T_IDENT T_ASSIGNED_STATE state_op fsm_state opt_timeout + { + $$ = make_cmd(CMD_WAIT_STATE); + $$->kind = CMD_ASSERT_ASSIGNED; + strlcpy($$->service, $3, sizeof($$->service)); + strlcpy($$->state, $6, sizeof($$->state)); + $$->timeoutSeconds = $7; + free($3); + } + | T_WAIT T_UNTIL T_IDENT T_ASSIGNED_STATE state_op T_IDENT opt_timeout + { + $$ = make_cmd(CMD_WAIT_STATE); + $$->kind = CMD_ASSERT_ASSIGNED; + strlcpy($$->service, $3, sizeof($$->service)); + strlcpy($$->state, $6, sizeof($$->state)); + $$->timeoutSeconds = $7; + free($3); free($6); + } + | T_WAIT T_UNTIL T_IDENT T_STOPPED opt_timeout + { + $$ = make_cmd(CMD_WAIT_STOPPED); + strlcpy($$->service, $3, sizeof($$->service)); + $$->timeoutSeconds = $5; + free($3); + } + | T_WAIT T_UNTIL state_name_list opt_in_group opt_timeout + { + $$ = current_wait_cmd; + $$->timeoutSeconds = $5; + current_wait_cmd = NULL; + } + /* + * Multi-condition form: + * wait until node2 state is primary and node1 state is secondary timeout 90s + * wait until node2 state is primary and node1 state is secondary with timeout 90s + * + * Parsed via wait_multi_condition_list which populates current_wait_cmd. + * Requires at least two conditions (single-condition uses the forms above). + */ + | T_WAIT T_UNTIL wait_multi_condition T_AND wait_multi_condition_list opt_timeout + { + $$ = current_wait_cmd; + $$->timeoutSeconds = $6; + current_wait_cmd = NULL; + } + ; + +/* + * state_name_list — one or more FSM state names separated by commas. + * + * Accepts both T_FS_* tokens (proper FSM state names) and T_IDENT + * (for forward-compatibility with unknown states). + */ +state_name_list: + fsm_state + { + current_wait_cmd = make_cmd(CMD_WAIT_STATES); + strlcpy(current_wait_cmd->waitStates[current_wait_cmd->waitStateCount++], + $1, sizeof(current_wait_cmd->waitStates[0])); + } + | T_IDENT + { + current_wait_cmd = make_cmd(CMD_WAIT_STATES); + strlcpy(current_wait_cmd->waitStates[current_wait_cmd->waitStateCount++], + $1, sizeof(current_wait_cmd->waitStates[0])); + free($1); + } + | state_name_list T_COMMA fsm_state + { + if (current_wait_cmd->waitStateCount < PGAF_MAX_WAIT_STATES) + strlcpy(current_wait_cmd->waitStates[current_wait_cmd->waitStateCount++], + $3, sizeof(current_wait_cmd->waitStates[0])); + } + | state_name_list T_COMMA T_IDENT + { + if (current_wait_cmd->waitStateCount < PGAF_MAX_WAIT_STATES) + strlcpy(current_wait_cmd->waitStates[current_wait_cmd->waitStateCount++], + $3, sizeof(current_wait_cmd->waitStates[0])); + free($3); + } + ; + +/* + * opt_in_group — optional "in group N [, group M ...]" clause. + * When absent, waitGroupCount stays 0 which means "all groups". + */ +opt_in_group: + /* empty */ + | T_IN group_items + ; + +group_items: + T_GROUP T_INTEGER + { + if (current_wait_cmd->waitGroupCount < PGAF_MAX_WAIT_GROUPS) + current_wait_cmd->waitGroups[current_wait_cmd->waitGroupCount++] = $2; + } + | group_items T_COMMA T_GROUP T_INTEGER + { + if (current_wait_cmd->waitGroupCount < PGAF_MAX_WAIT_GROUPS) + current_wait_cmd->waitGroups[current_wait_cmd->waitGroupCount++] = $4; + } + ; + +opt_timeout: + /* empty */ { $$ = PGAF_TIMEOUT_DEFAULT; } + | T_TIMEOUT T_INTEGER { $$ = $2; } + | T_WITH T_TIMEOUT T_INTEGER { $$ = $3; } + ; + +/* ----------------------------------------------------------------------- + * assert state = (instant check, no timeout) + * assert state is (same, alternate keyword) + * assert state is timeout Ns (polling wait, alias for wait until) + * assert assigned-state = + * ----------------------------------------------------------------------- */ + +assert_cmd: + T_ASSERT T_IDENT T_STATE state_op fsm_state opt_timeout + { + $$ = make_cmd($6 > 0 ? CMD_WAIT_STATE : CMD_ASSERT_STATE); + strlcpy($$->service, $2, sizeof($$->service)); + strlcpy($$->state, $5, sizeof($$->state)); + $$->timeoutSeconds = $6; + free($2); + } + | T_ASSERT T_IDENT T_STATE state_op T_IDENT opt_timeout + { + $$ = make_cmd($6 > 0 ? CMD_WAIT_STATE : CMD_ASSERT_STATE); + strlcpy($$->service, $2, sizeof($$->service)); + strlcpy($$->state, $5, sizeof($$->state)); + $$->timeoutSeconds = $6; + free($2); free($5); + } + | T_ASSERT T_IDENT T_ASSIGNED_STATE state_op fsm_state opt_timeout + { + $$ = make_cmd(CMD_ASSERT_ASSIGNED); + strlcpy($$->service, $2, sizeof($$->service)); + strlcpy($$->state, $5, sizeof($$->state)); + $$->timeoutSeconds = $6; + free($2); + } + | T_ASSERT T_IDENT T_ASSIGNED_STATE state_op T_IDENT opt_timeout + { + $$ = make_cmd(CMD_ASSERT_ASSIGNED); + strlcpy($$->service, $2, sizeof($$->service)); + strlcpy($$->state, $5, sizeof($$->state)); + $$->timeoutSeconds = $6; + free($2); free($5); + } + ; + +/* ----------------------------------------------------------------------- + * sql { SQL text } + * + * Inside STEP_BODY, '{' triggers the raw block reader which returns + * T_BLOCK with the SQL content already trimmed (braces not included). + * ----------------------------------------------------------------------- */ + +sql_cmd: + T_SQL T_IDENT T_BLOCK + { + $$ = make_cmd(CMD_SQL); + strlcpy($$->service, $2, sizeof($$->service)); + strlcpy($$->args, $3, sizeof($$->args)); + free($2); free($3); + } + ; + +/* ----------------------------------------------------------------------- + * expect { text } + * expect error [SQLSTATE] + * ----------------------------------------------------------------------- */ + +expect_cmd: + T_EXPECT T_BLOCK + { + $$ = make_cmd(CMD_EXPECT); + strlcpy($$->expected, $2, sizeof($$->expected)); + expand_tuple_expect($$->expected, sizeof($$->expected)); + free($2); + } + | T_EXPECT T_ERROR + { + $$ = make_cmd(CMD_EXPECT_ERROR); + } + | T_EXPECT T_ERROR T_IDENT + { + $$ = make_cmd(CMD_EXPECT_ERROR); + strlcpy($$->state, $3, sizeof($$->state)); + free($3); + } + | T_EXPECT T_ERROR T_INTEGER + { + /* SQLSTATE codes like 25006 are all digits, lexed as T_INTEGER */ + $$ = make_cmd(CMD_EXPECT_ERROR); + snprintf($$->state, sizeof($$->state), "%d", $3); + } + ; + +/* ----------------------------------------------------------------------- + * promote node1 [, node2, ...] + * ----------------------------------------------------------------------- */ + +promote_cmd: + T_PROMOTE promote_list + { + $$ = current_promote_cmd; + current_promote_cmd = NULL; + } + ; + +promote_list: + T_IDENT + { + current_promote_cmd = make_cmd(CMD_PROMOTE); + current_promote_cmd->timeoutSeconds = PGAF_TIMEOUT_DEFAULT; + strlcpy(current_promote_cmd->promoteNodes[current_promote_cmd->promoteCount++], + $1, sizeof(current_promote_cmd->promoteNodes[0])); + free($1); + } + | promote_list T_COMMA T_IDENT + { + if (current_promote_cmd->promoteCount < PGAF_MAX_PROMOTE_NODES) + strlcpy(current_promote_cmd->promoteNodes[current_promote_cmd->promoteCount++], + $3, sizeof(current_promote_cmd->promoteNodes[0])); + free($3); + } + ; + +/* ----------------------------------------------------------------------- + * network disconnect + * network connect + * ----------------------------------------------------------------------- */ + +network_cmd: + T_NETWORK T_DISCONNECT T_IDENT + { + $$ = make_cmd(CMD_NETWORK_OFF); + strlcpy($$->service, $3, sizeof($$->service)); + free($3); + } + | T_NETWORK T_CONNECT T_IDENT + { + $$ = make_cmd(CMD_NETWORK_ON); + strlcpy($$->service, $3, sizeof($$->service)); + free($3); + } + ; + +/* ----------------------------------------------------------------------- + * sleep Ns + * ----------------------------------------------------------------------- */ + +sleep_cmd: + T_SLEEP T_INTEGER + { + $$ = make_cmd(CMD_SLEEP); + $$->timeoutSeconds = $2; + } + ; + +/* ----------------------------------------------------------------------- + * compose down + * compose start + * compose stop + * ----------------------------------------------------------------------- */ + +compose_cmd: + T_COMPOSE T_DOWN + { + $$ = make_cmd(CMD_COMPOSE_DOWN); + } + | T_COMPOSE T_START T_IDENT + { + $$ = make_cmd(CMD_COMPOSE_START); + strlcpy($$->service, $3, sizeof($$->service)); + free($3); + } + | T_COMPOSE T_STOP T_IDENT + { + $$ = make_cmd(CMD_COMPOSE_STOP); + strlcpy($$->service, $3, sizeof($$->service)); + free($3); + } + | T_COMPOSE T_KILL T_IDENT + { + $$ = make_cmd(CMD_COMPOSE_KILL); + strlcpy($$->service, $3, sizeof($$->service)); + free($3); + } + /* + * compose inject : + * + * Injects a file from a Docker image into a running service container + * without restarting it. The sequence: + * docker create --name _pgaf_inject_tmp + * docker cp _pgaf_inject_tmp: /tmp/_pgaf_inject_binary + * docker rm _pgaf_inject_tmp + * docker cp /tmp/_pgaf_inject_binary --1: + * + * Fields used: + * expected → image name (e.g. "pgaf:next") + * args → source path in the image (e.g. "/usr/local/bin/pg_autoctl") + * service → destination service name (e.g. "monitor") + * state → destination path in the container (same as src usually) + * + * T_INJECT triggers BEGIN(EXEC_ARGS) in the lexer, so the image name is + * returned as T_IDENT (matching [^ \t\n]+, which includes ':') and the + * remaining "src svc:dst" tokens come back as T_SHELL_ARGS. + */ + | T_COMPOSE T_INJECT T_IDENT T_SHELL_ARGS + { + $$ = make_cmd(CMD_COMPOSE_INJECT); + strlcpy($$->expected, $3, sizeof($$->expected)); /* image */ + + /* Split T_SHELL_ARGS: " :" */ + char tmp[4096]; + strlcpy(tmp, $4, sizeof(tmp)); + char *src = tmp; + char *p = tmp; + while (*p && *p != ' ' && *p != '\t') p++; + if (*p) { *p++ = '\0'; while (*p == ' ' || *p == '\t') p++; } + char *svcdst = p; + char *colon = (*svcdst) ? strchr(svcdst, ':') : NULL; + strlcpy($$->args, src, sizeof($$->args)); + if (colon) + { + *colon = '\0'; + strlcpy($$->service, svcdst, sizeof($$->service)); /* dst svc */ + strlcpy($$->state, colon + 1, sizeof($$->state)); /* dst path */ + } + free($3); free($4); + } + ; + +/* ----------------------------------------------------------------------- + * stop postgres / start postgres + * + * Sugar for `pg_autoctl manual pgctl off/on --pgdata /var/lib/postgres/pgaf` + * run inside the named node's container. Much cleaner than the raw pg_ctl + * loop that was used before. + * ----------------------------------------------------------------------- */ + +postgres_ctl_cmd: + T_STOP T_POSTGRES node_name + { + $$ = make_cmd(CMD_STOP_POSTGRES); + strlcpy($$->service, $3, sizeof($$->service)); + free($3); + } + | T_START T_POSTGRES node_name + { + $$ = make_cmd(CMD_START_POSTGRES); + strlcpy($$->service, $3, sizeof($$->service)); + free($3); + } + ; + +/* ----------------------------------------------------------------------- + * assert stays while { commands } + * + * Runs the body commands and checks after each one (and at the end) that + * the named node's reported state equals . Fails immediately if the + * state ever changes. + * ----------------------------------------------------------------------- */ + +while_body: + T_WHILE { pgaf_next_brace_is_while = 1; } T_LBRACE cmd_list T_RBRACE + { $$ = $4; } + ; + +stays_while_cmd: + T_ASSERT node_name T_STAYS fsm_state while_body + { + $$ = make_cmd(CMD_STAYS_WHILE); + strlcpy($$->service, $2, sizeof($$->service)); + strlcpy($$->state, $4, sizeof($$->state)); + $$->body = ($5) ? $5->commands : NULL; + free($2); + } + ; + +/* ----------------------------------------------------------------------- + * set monitor + * + * Switches the runner's active monitor: LISTEN/NOTIFY reconnects to the + * new service, and monitor_get_node_state() queries it for subsequent + * wait-until / implicit post-failover checks. + * ----------------------------------------------------------------------- */ + +set_monitor_cmd: + T_SET T_IDENT T_IDENT + { + /* only "set monitor " is supported; $2 must be "monitor" */ + if (strcmp($2, "monitor") != 0) + { + fprintf(stderr, "pgaftest: unknown 'set' target '%s' (expected 'monitor')\n", $2); + free($2); free($3); + YYERROR; + } + $$ = make_cmd(CMD_SET_MONITOR); + strlcpy($$->service, $3, sizeof($$->service)); + free($2); free($3); + } + ; + +/* ----------------------------------------------------------------------- + * logs [not] contains + * logs [not] matches + * + * Grep the container's log output (via docker compose logs) for . + * "contains" uses fixed-string grep; "matches" uses grep -P (PCRE). + * With "not", the check asserts the pattern is NOT found. + * ----------------------------------------------------------------------- */ + +logs_cmd: + T_LOGS T_IDENT T_CONTAINS T_STRING + { + $$ = make_cmd(CMD_LOGS_CHECK); + strlcpy($$->service, $2, sizeof($$->service)); + strlcpy($$->args, $4, sizeof($$->args)); + $$->logsNegate = false; + $$->allowError = false; /* false = fixed string, true = PCRE */ + free($2); free($4); + } + | T_LOGS T_IDENT T_NOT T_CONTAINS T_STRING + { + $$ = make_cmd(CMD_LOGS_CHECK); + strlcpy($$->service, $2, sizeof($$->service)); + strlcpy($$->args, $5, sizeof($$->args)); + $$->logsNegate = true; + $$->allowError = false; + free($2); free($5); + } + | T_LOGS T_IDENT T_MATCHES T_STRING + { + $$ = make_cmd(CMD_LOGS_CHECK); + strlcpy($$->service, $2, sizeof($$->service)); + strlcpy($$->args, $4, sizeof($$->args)); + $$->logsNegate = false; + $$->allowError = true; /* true = PCRE (-P) */ + free($2); free($4); + } + | T_LOGS T_IDENT T_NOT T_MATCHES T_STRING + { + $$ = make_cmd(CMD_LOGS_CHECK); + strlcpy($$->service, $2, sizeof($$->service)); + strlcpy($$->args, $5, sizeof($$->args)); + $$->logsNegate = true; + $$->allowError = true; + free($2); free($5); + } + ; + +/* ----------------------------------------------------------------------- + * sequence + * ----------------------------------------------------------------------- */ + +sequence_block: + T_SEQUENCE sequence_names + ; + +sequence_names: + /* empty */ + | sequence_names ident_or_string + { + int i = current_spec->sequenceLength; + if (i < PGAF_MAX_SEQ) + current_spec->sequence[current_spec->sequenceLength++] = $2; + else + { + fprintf(stderr, "pgaftest: too many steps in sequence (max %d)\n", + PGAF_MAX_SEQ); + exit(1); + } + } + ; + +/* ----------------------------------------------------------------------- + * fsm_state — matches any FSM state token and returns its canonical name. + * + * Use %type so callers can strlcpy the name into their cmd->state + * field. The returned string is a string literal — do NOT free() it. + * ----------------------------------------------------------------------- */ + +fsm_state: + T_FS_INIT { $$ = "init"; } + | T_FS_SINGLE { $$ = "single"; } + | T_FS_PRIMARY { $$ = "primary"; } + | T_FS_WAIT_PRIMARY { $$ = "wait_primary"; } + | T_FS_WAIT_STANDBY { $$ = "wait_standby"; } + | T_FS_DEMOTED { $$ = "demoted"; } + | T_FS_DEMOTE_TIMEOUT { $$ = "demote_timeout"; } + | T_FS_DRAINING { $$ = "draining"; } + | T_FS_SECONDARY { $$ = "secondary"; } + | T_FS_CATCHINGUP { $$ = "catchingup"; } + | T_FS_PREP_PROMOTION { $$ = "prepare_promotion"; } + | T_FS_STOP_REPLICATION { $$ = "stop_replication"; } + | T_FS_MAINTENANCE { $$ = "maintenance"; } + | T_FS_JOIN_PRIMARY { $$ = "join_primary"; } + | T_FS_APPLY_SETTINGS { $$ = "apply_settings"; } + | T_FS_PREPARE_MAINTENANCE { $$ = "prepare_maintenance"; } + | T_FS_WAIT_MAINTENANCE { $$ = "wait_maintenance"; } + | T_FS_REPORT_LSN { $$ = "report_lsn"; } + | T_FS_FAST_FORWARD { $$ = "fast_forward"; } + | T_FS_JOIN_SECONDARY { $$ = "join_secondary"; } + | T_FS_DROPPED { $$ = "dropped"; } + ; + +/* ----------------------------------------------------------------------- + * Helpers + * ----------------------------------------------------------------------- */ + +ident_or_string: + T_IDENT { $$ = $1; } + | T_STRING { $$ = $1; } + ; + +%% + +/* ----------------------------------------------------------------------- + * Public entry point + * ----------------------------------------------------------------------- */ + +TestSpec * +parse_test_spec(const char *filename) +{ + FILE *f = fopen(filename, "r"); + if (!f) + { + fprintf(stderr, "pgaftest: cannot open spec file \"%s\": %s\n", + filename, strerror(errno)); + return NULL; + } + + TestSpec *spec = (TestSpec *) calloc(1, sizeof(TestSpec)); + if (!spec) { fprintf(stderr, "out of memory\n"); exit(1); } + + strncpy(spec->filename, filename, sizeof(spec->filename)-1); + + current_spec = spec; + pgaf_line_number = 1; + yyin = f; + yyparse(); + fclose(f); + + return spec; +} + +TestCmd * +make_cmd(TestCmdKind kind) +{ + TestCmd *c = (TestCmd *) calloc(1, sizeof(TestCmd)); + if (!c) { fprintf(stderr, "out of memory\n"); exit(1); } + c->kind = kind; + c->timeoutSeconds = PGAF_TIMEOUT_DEFAULT; + return c; +} + +TestStep * +make_step(const char *name) +{ + TestStep *s = (TestStep *) calloc(1, sizeof(TestStep)); + if (!s) { fprintf(stderr, "out of memory\n"); exit(1); } + if (name) strncpy(s->name, name, sizeof(s->name)-1); + return s; +} + +TestStep * +spec_find_step(TestSpec *spec, const char *name) +{ + for (TestStep *s = spec->steps; s; s = s->next) + { + if (strcmp(s->name, name) == 0) + return s; + } + return NULL; +} diff --git a/src/bin/pgaftest/test_spec_scan.c b/src/bin/pgaftest/test_spec_scan.c new file mode 100644 index 000000000..e93ed2785 --- /dev/null +++ b/src/bin/pgaftest/test_spec_scan.c @@ -0,0 +1,3912 @@ +#line 1 "test_spec_scan.c" + +#line 3 "test_spec_scan.c" + +#define YY_INT_ALIGNED short int + +/* A lexical scanner generated by flex */ + +#define FLEX_SCANNER +#define YY_FLEX_MAJOR_VERSION 2 +#define YY_FLEX_MINOR_VERSION 6 +#define YY_FLEX_SUBMINOR_VERSION 4 +#if YY_FLEX_SUBMINOR_VERSION > 0 +#define FLEX_BETA +#endif + +/* First, we deal with platform-specific or compiler-specific issues. */ + +/* begin standard C headers. */ +#include +#include +#include +#include + +/* end standard C headers. */ + +/* flex integer type definitions */ + +#ifndef FLEXINT_H +#define FLEXINT_H + +/* C99 systems have . Non-C99 systems may or may not. */ + +#if defined (__STDC_VERSION__) && __STDC_VERSION__ >= 199901L + +/* C99 says to define __STDC_LIMIT_MACROS before including stdint.h, + * if you want the limit (max/min) macros for int types. + */ +#ifndef __STDC_LIMIT_MACROS +#define __STDC_LIMIT_MACROS 1 +#endif + +#include +typedef int8_t flex_int8_t; +typedef uint8_t flex_uint8_t; +typedef int16_t flex_int16_t; +typedef uint16_t flex_uint16_t; +typedef int32_t flex_int32_t; +typedef uint32_t flex_uint32_t; +typedef uint64_t flex_uint64_t; +#else +typedef signed char flex_int8_t; +typedef short int flex_int16_t; +typedef int flex_int32_t; +typedef unsigned char flex_uint8_t; +typedef unsigned short int flex_uint16_t; +typedef unsigned int flex_uint32_t; + +/* Limits of integral types. */ +#ifndef INT8_MIN +#define INT8_MIN (-128) +#endif +#ifndef INT16_MIN +#define INT16_MIN (-32767 - 1) +#endif +#ifndef INT32_MIN +#define INT32_MIN (-2147483647 - 1) +#endif +#ifndef INT8_MAX +#define INT8_MAX (127) +#endif +#ifndef INT16_MAX +#define INT16_MAX (32767) +#endif +#ifndef INT32_MAX +#define INT32_MAX (2147483647) +#endif +#ifndef UINT8_MAX +#define UINT8_MAX (255U) +#endif +#ifndef UINT16_MAX +#define UINT16_MAX (65535U) +#endif +#ifndef UINT32_MAX +#define UINT32_MAX (4294967295U) +#endif + +#ifndef SIZE_MAX +#define SIZE_MAX (~(size_t) 0) +#endif + +#endif /* ! C99 */ + +#endif /* ! FLEXINT_H */ + +/* begin standard C++ headers. */ + +/* TODO: this is always defined, so inline it */ +#define yyconst const + +#if defined(__GNUC__) && __GNUC__ >= 3 +#define yynoreturn __attribute__((__noreturn__)) +#else +#define yynoreturn +#endif + +/* Returned upon end-of-file. */ +#define YY_NULL 0 + +/* Promotes a possibly negative, possibly signed char to an + * integer in range [0..255] for use as an array index. + */ +#define YY_SC_TO_UI(c) ((YY_CHAR) (c)) + +/* Enter a start condition. This macro really ought to take a parameter, + * but we do it the disgusting crufty way forced on us by the ()-less + * definition of BEGIN. + */ +#define BEGIN (yy_start) = 1 + 2 * + +/* Translate the current start state into a value that can be later handed + * to BEGIN to return to the state. The YYSTATE alias is for lex + * compatibility. + */ +#define YY_START (((yy_start) - 1) / 2) +#define YYSTATE YY_START + +/* Action number for EOF rule of a given start state. */ +#define YY_STATE_EOF(state) (YY_END_OF_BUFFER + state + 1) + +/* Special action meaning "start processing a new file". */ +#define YY_NEW_FILE yyrestart(yyin) +#define YY_END_OF_BUFFER_CHAR 0 + +/* Size of default input buffer. */ +#ifndef YY_BUF_SIZE +#ifdef __ia64__ + +/* On IA-64, the buffer size is 16k, not 8k. + * Moreover, YY_BUF_SIZE is 2*YY_READ_BUF_SIZE in the general case. + * Ditto for the __ia64__ case accordingly. + */ +#define YY_BUF_SIZE 32768 +#else +#define YY_BUF_SIZE 16384 +#endif /* __ia64__ */ +#endif + +/* The state buf must be large enough to hold one state per character in the main buffer. + */ +#define YY_STATE_BUF_SIZE ((YY_BUF_SIZE + 2) * sizeof(yy_state_type)) + +#ifndef YY_TYPEDEF_YY_BUFFER_STATE +#define YY_TYPEDEF_YY_BUFFER_STATE +typedef struct yy_buffer_state *YY_BUFFER_STATE; +#endif + +#ifndef YY_TYPEDEF_YY_SIZE_T +#define YY_TYPEDEF_YY_SIZE_T +typedef size_t yy_size_t; +#endif + +extern yy_size_t yyleng; + +extern FILE *yyin, *yyout; + +#define EOB_ACT_CONTINUE_SCAN 0 +#define EOB_ACT_END_OF_FILE 1 +#define EOB_ACT_LAST_MATCH 2 + + #define YY_LESS_LINENO(n) + #define YY_LINENO_REWIND_TO(ptr) + +/* Return all but the first "n" matched characters back to the input stream. */ +#define yyless(n) \ + do \ + { \ + /* Undo effects of setting up yytext. */ \ + int yyless_macro_arg = (n); \ + YY_LESS_LINENO(yyless_macro_arg); \ + *yy_cp = (yy_hold_char); \ + YY_RESTORE_YY_MORE_OFFSET \ + (yy_c_buf_p) = yy_cp = yy_bp + yyless_macro_arg - YY_MORE_ADJ; \ + YY_DO_BEFORE_ACTION; /* set up yytext again */ \ + } \ + while (0) +#define unput(c) yyunput(c, (yytext_ptr)) + +#ifndef YY_STRUCT_YY_BUFFER_STATE +#define YY_STRUCT_YY_BUFFER_STATE +struct yy_buffer_state +{ + FILE *yy_input_file; + + char *yy_ch_buf; /* input buffer */ + char *yy_buf_pos; /* current position in input buffer */ + + /* Size of input buffer in bytes, not including room for EOB + * characters. + */ + int yy_buf_size; + + /* Number of characters read into yy_ch_buf, not including EOB + * characters. + */ + yy_size_t yy_n_chars; + + /* Whether we "own" the buffer - i.e., we know we created it, + * and can realloc() it to grow it, and should free() it to + * delete it. + */ + int yy_is_our_buffer; + + /* Whether this is an "interactive" input source; if so, and + * if we're using stdio for input, then we want to use getc() + * instead of fread(), to make sure we stop fetching input after + * each newline. + */ + int yy_is_interactive; + + /* Whether we're considered to be at the beginning of a line. + * If so, '^' rules will be active on the next match, otherwise + * not. + */ + int yy_at_bol; + + int yy_bs_lineno; /**< The line count. */ + int yy_bs_column; /**< The column count. */ + + /* Whether to try to fill the input buffer when we reach the + * end of it. + */ + int yy_fill_buffer; + + int yy_buffer_status; + +#define YY_BUFFER_NEW 0 +#define YY_BUFFER_NORMAL 1 + + /* When an EOF's been seen but there's still some text to process + * then we mark the buffer as YY_EOF_PENDING, to indicate that we + * shouldn't try reading from the input source any more. We might + * still have a bunch of tokens to match, though, because of + * possible backing-up. + * + * When we actually see the EOF, we change the status to "new" + * (via yyrestart()), so that the user can continue scanning by + * just pointing yyin at a new input file. + */ +#define YY_BUFFER_EOF_PENDING 2 +}; +#endif /* !YY_STRUCT_YY_BUFFER_STATE */ + +/* Stack of input buffers. */ +static size_t yy_buffer_stack_top = 0; /**< index of top of stack. */ +static size_t yy_buffer_stack_max = 0; /**< capacity of stack. */ +static YY_BUFFER_STATE *yy_buffer_stack = NULL; /**< Stack as an array. */ + +/* We provide macros for accessing buffer states in case in the + * future we want to put the buffer states in a more general + * "scanner state". + * + * Returns the top of the stack, or NULL. + */ +#define YY_CURRENT_BUFFER ((yy_buffer_stack) \ + ? (yy_buffer_stack)[(yy_buffer_stack_top)] \ + : NULL) + +/* Same as previous macro, but useful when we know that the buffer stack is not + * NULL or when we need an lvalue. For internal use only. + */ +#define YY_CURRENT_BUFFER_LVALUE (yy_buffer_stack)[(yy_buffer_stack_top)] + +/* yy_hold_char holds the character lost when yytext is formed. */ +static char yy_hold_char; +static yy_size_t yy_n_chars; /* number of characters read into yy_ch_buf */ +yy_size_t yyleng; + +/* Points to current character in buffer. */ +static char *yy_c_buf_p = NULL; +static int yy_init = 0; /* whether we need to initialize */ +static int yy_start = 0; /* start state number */ + +/* Flag which is used to allow yywrap()'s to do buffer switches + * instead of setting up a fresh yyin. A bit of a hack ... + */ +static int yy_did_buffer_switch_on_eof; + +void yyrestart(FILE *input_file); +void yy_switch_to_buffer(YY_BUFFER_STATE new_buffer); +YY_BUFFER_STATE yy_create_buffer(FILE *file, int size); +void yy_delete_buffer(YY_BUFFER_STATE b); +void yy_flush_buffer(YY_BUFFER_STATE b); +void yypush_buffer_state(YY_BUFFER_STATE new_buffer); +void yypop_buffer_state(void); + +static void yyensure_buffer_stack(void); +static void yy_load_buffer_state(void); +static void yy_init_buffer(YY_BUFFER_STATE b, FILE *file); +#define YY_FLUSH_BUFFER yy_flush_buffer(YY_CURRENT_BUFFER) + +YY_BUFFER_STATE yy_scan_buffer(char *base, yy_size_t size); +YY_BUFFER_STATE yy_scan_string(const char *yy_str); +YY_BUFFER_STATE yy_scan_bytes(const char *bytes, yy_size_t len); + +void * yyalloc(yy_size_t); +void * yyrealloc(void *, yy_size_t); +void yyfree(void *); + +#define yy_new_buffer yy_create_buffer +#define yy_set_interactive(is_interactive) \ + { \ + if (!YY_CURRENT_BUFFER) { \ + yyensure_buffer_stack(); \ + YY_CURRENT_BUFFER_LVALUE = \ + yy_create_buffer(yyin, YY_BUF_SIZE); \ + } \ + YY_CURRENT_BUFFER_LVALUE->yy_is_interactive = is_interactive; \ + } +#define yy_set_bol(at_bol) \ + { \ + if (!YY_CURRENT_BUFFER) { \ + yyensure_buffer_stack(); \ + YY_CURRENT_BUFFER_LVALUE = \ + yy_create_buffer(yyin, YY_BUF_SIZE); \ + } \ + YY_CURRENT_BUFFER_LVALUE->yy_at_bol = at_bol; \ + } +#define YY_AT_BOL() (YY_CURRENT_BUFFER_LVALUE->yy_at_bol) + +/* Begin user sect3 */ + +#define yywrap() (/*CONSTCOND*/ 1) +#define YY_SKIP_YYWRAP +typedef flex_uint8_t YY_CHAR; + +FILE *yyin = NULL, *yyout = NULL; + +typedef int yy_state_type; + +extern int yylineno; +int yylineno = 1; + +extern char *yytext; +#ifdef yytext_ptr +#undef yytext_ptr +#endif +#define yytext_ptr yytext + +static yy_state_type yy_get_previous_state(void); +static yy_state_type yy_try_NUL_trans(yy_state_type current_state); +static int yy_get_next_buffer(void); +static void yynoreturn yy_fatal_error(const char *msg); + +/* Done after the current pattern has been matched and before the + * corresponding action - sets up yytext. + */ +#define YY_DO_BEFORE_ACTION \ + (yytext_ptr) = yy_bp; \ + yyleng = (yy_size_t) (yy_cp - yy_bp); \ + (yy_hold_char) = *yy_cp; \ + *yy_cp = '\0'; \ + (yy_c_buf_p) = yy_cp; +#define YY_NUM_RULES 147 +#define YY_END_OF_BUFFER 148 + +/* This struct is not used in this scanner, + * but its presence is necessary. */ +struct yy_trans_info +{ + flex_int32_t yy_verify; + flex_int32_t yy_nxt; +}; +static const flex_int16_t yy_accept[1162] = +{ + 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 148, 18, 17, 16, 18, 1, 12, 11, 13, 13, + 13, 13, 13, 13, 15, 147, 21, 20, 147, 19, + 55, 54, 92, 92, 92, 92, 92, 92, 92, 92, + 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, + 57, 58, 95, 94, 147, 93, 126, 137, 125, 141, + 141, 141, 141, 141, 141, 141, 141, 141, 141, 141, + 141, 141, 141, 141, 141, 141, 141, 141, 139, 140, + 143, 142, 144, 145, 146, 17, 0, 14, 1, 12, + 12, 13, 13, 13, 13, 13, 13, 13, 13, 21, + + 0, 56, 19, 55, 55, 92, 92, 92, 92, 92, + 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, + 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, + 92, 92, 92, 92, 92, 92, 92, 92, 92, 95, + 0, 138, 93, 137, 137, 141, 141, 141, 141, 141, + 141, 141, 141, 141, 141, 141, 141, 141, 141, 117, + 123, 141, 141, 141, 141, 141, 141, 141, 141, 141, + 141, 141, 141, 141, 141, 141, 141, 141, 141, 141, + 141, 141, 143, 142, 145, 13, 13, 13, 13, 13, + 13, 13, 13, 92, 92, 92, 92, 92, 92, 92, + + 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, + 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, + 92, 92, 92, 92, 92, 92, 92, 92, 25, 92, + 92, 92, 92, 122, 141, 141, 141, 141, 141, 141, + 141, 141, 141, 141, 141, 141, 141, 141, 141, 141, + 141, 141, 141, 141, 141, 141, 141, 141, 134, 141, + 141, 141, 141, 141, 141, 141, 131, 141, 141, 103, + 141, 141, 141, 141, 141, 141, 141, 141, 13, 13, + 13, 4, 13, 13, 9, 13, 92, 92, 27, 92, + 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, + + 92, 92, 92, 92, 92, 59, 92, 92, 92, 92, + 92, 92, 30, 92, 92, 47, 92, 92, 92, 92, + 92, 92, 92, 92, 92, 92, 141, 141, 141, 141, + 141, 141, 141, 141, 141, 141, 112, 141, 141, 141, + 97, 141, 141, 141, 59, 141, 141, 116, 133, 141, + 141, 141, 141, 141, 141, 141, 141, 141, 141, 141, + 141, 141, 141, 141, 114, 141, 141, 141, 99, 141, + 124, 13, 13, 13, 13, 7, 13, 92, 33, 92, + 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, + 92, 92, 92, 92, 46, 24, 92, 92, 92, 92, + + 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, + 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, + 141, 141, 141, 141, 141, 141, 141, 141, 141, 141, + 141, 141, 105, 141, 141, 141, 141, 121, 141, 141, + 141, 141, 141, 141, 141, 141, 141, 141, 141, 141, + 141, 141, 110, 113, 118, 128, 141, 141, 141, 141, + 141, 100, 141, 141, 129, 13, 13, 13, 13, 13, + 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, + 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, + 92, 92, 92, 92, 37, 43, 92, 92, 92, 92, + + 92, 92, 92, 92, 92, 92, 60, 92, 92, 92, + 42, 92, 92, 92, 92, 92, 92, 32, 141, 141, + 102, 141, 141, 141, 141, 141, 141, 141, 141, 141, + 141, 141, 104, 141, 141, 132, 141, 141, 141, 141, + 141, 141, 141, 141, 141, 141, 141, 141, 141, 141, + 60, 141, 141, 141, 141, 141, 141, 141, 141, 141, + 141, 141, 13, 13, 2, 3, 13, 13, 92, 92, + 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, + 92, 66, 92, 91, 92, 92, 92, 92, 92, 92, + 92, 92, 92, 92, 92, 92, 22, 92, 92, 92, + + 92, 61, 92, 92, 92, 92, 92, 92, 41, 92, + 92, 92, 92, 92, 92, 141, 141, 141, 141, 141, + 111, 109, 141, 141, 141, 66, 141, 141, 91, 141, + 141, 141, 141, 141, 141, 141, 141, 136, 107, 141, + 141, 141, 61, 106, 141, 141, 141, 141, 141, 115, + 130, 101, 141, 141, 141, 141, 141, 141, 13, 13, + 10, 8, 92, 92, 92, 92, 92, 92, 92, 92, + 92, 92, 38, 92, 92, 69, 92, 92, 92, 92, + 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, + 29, 35, 92, 92, 92, 92, 92, 92, 92, 92, + + 92, 92, 92, 92, 92, 92, 141, 141, 141, 141, + 141, 135, 141, 141, 141, 69, 141, 141, 141, 141, + 141, 141, 141, 141, 141, 127, 141, 141, 141, 141, + 141, 141, 141, 141, 141, 141, 141, 141, 141, 13, + 13, 92, 92, 92, 92, 92, 92, 92, 92, 92, + 92, 92, 92, 92, 92, 92, 28, 92, 39, 40, + 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, + 92, 92, 92, 92, 70, 92, 92, 92, 92, 92, + 92, 92, 92, 141, 141, 141, 141, 141, 141, 141, + 141, 141, 141, 141, 141, 141, 141, 141, 141, 141, + + 141, 141, 141, 141, 141, 141, 70, 141, 141, 141, + 141, 141, 141, 141, 141, 13, 13, 92, 92, 92, + 92, 92, 71, 92, 92, 92, 92, 92, 92, 92, + 92, 92, 92, 92, 92, 92, 92, 92, 92, 34, + 92, 92, 92, 92, 92, 86, 85, 92, 92, 92, + 92, 92, 92, 92, 92, 141, 141, 141, 141, 71, + 141, 141, 108, 96, 141, 141, 141, 141, 141, 141, + 141, 98, 141, 141, 141, 141, 86, 85, 141, 141, + 141, 141, 141, 141, 141, 141, 13, 13, 92, 92, + 26, 53, 92, 92, 92, 31, 92, 92, 92, 92, + + 92, 92, 92, 92, 92, 92, 92, 76, 92, 92, + 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, + 92, 92, 141, 141, 141, 141, 141, 141, 141, 141, + 141, 141, 141, 141, 76, 141, 141, 141, 141, 141, + 141, 141, 141, 141, 141, 141, 141, 13, 6, 92, + 92, 92, 92, 92, 92, 92, 92, 92, 88, 87, + 23, 78, 92, 77, 92, 92, 92, 92, 92, 92, + 92, 92, 92, 92, 63, 65, 92, 62, 64, 141, + 141, 141, 141, 141, 141, 88, 87, 78, 141, 77, + 141, 141, 141, 141, 141, 141, 141, 141, 63, 65, + + 141, 62, 64, 13, 92, 92, 92, 92, 92, 92, + 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, + 92, 92, 92, 92, 92, 92, 141, 141, 141, 141, + 141, 141, 141, 141, 141, 141, 141, 141, 141, 141, + 141, 141, 13, 80, 79, 92, 92, 92, 49, 68, + 67, 92, 90, 89, 92, 92, 92, 92, 92, 92, + 92, 92, 92, 92, 92, 80, 79, 119, 141, 68, + 67, 90, 89, 141, 141, 141, 141, 141, 141, 141, + 141, 13, 92, 92, 44, 92, 92, 92, 92, 92, + 92, 92, 92, 92, 92, 92, 92, 141, 141, 141, + + 141, 141, 141, 141, 141, 141, 13, 92, 92, 92, + 36, 92, 92, 92, 92, 92, 92, 75, 74, 84, + 83, 141, 141, 141, 141, 141, 75, 74, 84, 83, + 5, 92, 92, 52, 92, 73, 92, 72, 92, 92, + 141, 141, 73, 141, 72, 45, 48, 92, 92, 92, + 50, 120, 141, 141, 82, 81, 92, 82, 81, 51, + 0 +}; + +static const YY_CHAR yy_ec[256] = +{ + 0, + 1, 1, 1, 1, 1, 1, 1, 1, 2, 3, + 1, 1, 4, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 2, 1, 5, 6, 1, 1, 1, 1, 1, + 1, 1, 1, 7, 8, 1, 1, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 1, 1, 1, + 10, 1, 1, 1, 11, 11, 11, 11, 11, 11, + 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, + 11, 11, 12, 11, 11, 11, 11, 11, 11, 11, + 1, 1, 1, 1, 13, 1, 14, 15, 16, 17, + + 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, + 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, + 38, 11, 39, 1, 40, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1 +}; + +static const YY_CHAR yy_meta[41] = +{ + 0, + 1, 2, 3, 1, 1, 1, 1, 4, 4, 1, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 4, 4, 4, 4, 4, 4, 4, 4, 1, 1 +}; + +static const flex_int16_t yy_base[1175] = +{ + 0, + 0, 0, 40, 0, 80, 0, 119, 121, 1250, 1249, + 1251, 1254, 123, 1254, 1245, 0, 117, 1254, 0, 106, + 1221, 1220, 112, 1229, 1254, 1254, 130, 1254, 1241, 0, + 124, 1254, 0, 106, 1223, 125, 119, 1207, 127, 1212, + 116, 1214, 130, 132, 120, 137, 1223, 139, 1212, 145, + 1254, 1254, 160, 1254, 1234, 0, 1254, 154, 1254, 0, + 147, 153, 160, 138, 1224, 1206, 153, 1208, 1213, 1206, + 1219, 159, 164, 1214, 171, 176, 1204, 185, 1254, 1254, + 0, 1228, 1254, 0, 1254, 198, 1224, 1254, 0, 196, + 1254, 0, 1195, 1193, 1199, 1208, 179, 1206, 1209, 209, + + 1217, 1254, 0, 205, 1254, 0, 1192, 1182, 1186, 1191, + 183, 1184, 1188, 200, 204, 1182, 1182, 1182, 1184, 144, + 1189, 1188, 1175, 1176, 1185, 1179, 186, 1179, 1172, 1172, + 202, 1173, 1185, 1173, 1174, 1170, 1172, 1174, 1164, 219, + 1189, 1254, 0, 213, 1254, 0, 1176, 1163, 1159, 200, + 203, 1164, 1157, 1152, 220, 1156, 213, 1154, 1157, 213, + 0, 1162, 1158, 1162, 216, 1148, 1147, 1166, 1146, 222, + 1148, 223, 1149, 1157, 1149, 227, 1142, 1146, 1138, 1148, + 1147, 1135, 0, 1165, 0, 1132, 1133, 1142, 1145, 1128, + 1127, 1131, 1128, 1133, 1130, 1135, 1138, 1137, 1137, 1118, + + 1120, 1128, 1131, 1120, 1125, 1117, 1127, 1111, 1117, 1108, + 1121, 1122, 1106, 1111, 1110, 1103, 1108, 1112, 1107, 1114, + 1123, 1098, 1096, 1099, 1101, 218, 1098, 1105, 0, 1095, + 1089, 1089, 1097, 0, 1095, 229, 1102, 1102, 1088, 225, + 1088, 1099, 1087, 1091, 1083, 1083, 1094, 1091, 1075, 1073, + 1073, 1087, 1077, 1078, 1070, 1074, 1084, 1063, 0, 1084, + 1064, 1067, 1069, 1068, 1065, 1064, 0, 1071, 1072, 0, + 226, 1060, 1060, 1069, 1064, 1052, 1059, 1062, 1050, 1048, + 1047, 0, 1061, 1049, 0, 1060, 1038, 1059, 1066, 1065, + 1050, 1050, 1038, 1052, 1054, 1036, 1033, 1038, 1035, 1036, + + 252, 1048, 1032, 1042, 1042, 1036, 253, 1041, 1038, 1022, + 1021, 1025, 0, 1020, 1015, 0, 1036, 1035, 1026, 1016, + 1019, 1020, 254, 1018, 255, 1025, 1004, 1010, 1020, 1017, + 1017, 1009, 1018, 1021, 1001, 1005, 0, 1005, 1002, 999, + 1021, 1012, 261, 998, 0, 1010, 262, 0, 0, 992, + 1003, 995, 988, 1001, 1006, 1005, 990, 986, 989, 990, + 985, 980, 994, 979, 263, 976, 981, 983, 264, 989, + 0, 998, 987, 976, 976, 0, 974, 265, 0, 975, + 968, 982, 976, 989, 974, 968, 963, 975, 970, 973, + 958, 970, 969, 954, 0, 978, 963, 970, 250, 252, + + 962, 955, 963, 952, 952, 940, 949, 945, 944, 958, + 940, 955, 953, 939, 938, 950, 949, 259, 261, 935, + 281, 932, 937, 946, 940, 929, 944, 937, 940, 930, + 934, 937, 0, 935, 920, 933, 932, 0, 917, 266, + 267, 931, 930, 916, 913, 914, 913, 912, 909, 908, + 923, 921, 0, 0, 0, 0, 907, 906, 918, 915, + 900, 0, 271, 275, 0, 270, 902, 901, 915, 894, + 897, 896, 909, 898, 911, 897, 286, 896, 914, 903, + 297, 893, 902, 896, 889, 888, 893, 881, 899, 887, + 880, 892, 878, 890, 0, 0, 880, 875, 883, 877, + + 872, 884, 863, 886, 300, 885, 0, 880, 879, 879, + 0, 881, 863, 860, 878, 860, 857, 0, 857, 856, + 0, 869, 872, 858, 866, 850, 855, 303, 854, 853, + 862, 864, 0, 849, 848, 0, 844, 856, 842, 854, + 844, 838, 845, 840, 849, 848, 827, 846, 304, 849, + 0, 844, 843, 843, 838, 825, 843, 825, 822, 840, + 822, 819, 823, 822, 0, 0, 831, 821, 829, 828, + 812, 810, 810, 822, 816, 822, 825, 822, 820, 803, + 802, 0, 814, 0, 805, 801, 800, 802, 815, 795, + 802, 804, 809, 802, 807, 808, 813, 787, 803, 801, + + 311, 0, 784, 791, 790, 783, 784, 783, 0, 789, + 788, 795, 786, 785, 792, 787, 786, 786, 769, 781, + 0, 0, 768, 766, 765, 0, 779, 776, 0, 773, + 763, 762, 770, 775, 768, 773, 774, 0, 0, 771, + 754, 313, 0, 0, 760, 759, 752, 753, 752, 0, + 0, 0, 758, 757, 764, 755, 754, 761, 746, 742, + 0, 0, 739, 738, 749, 738, 750, 733, 732, 749, + 731, 738, 0, 740, 739, 0, 733, 723, 722, 729, + 721, 733, 145, 164, 225, 228, 252, 282, 286, 294, + 0, 0, 299, 301, 301, 297, 299, 294, 308, 309, + + 308, 310, 310, 311, 313, 313, 308, 309, 335, 326, + 311, 0, 324, 325, 332, 0, 324, 314, 315, 326, + 325, 328, 327, 329, 324, 0, 332, 333, 328, 331, + 326, 340, 341, 340, 342, 342, 343, 345, 345, 342, + 350, 342, 343, 349, 362, 371, 351, 349, 354, 355, + 350, 359, 360, 379, 374, 375, 0, 370, 0, 0, + 377, 365, 379, 367, 379, 382, 366, 384, 368, 386, + 370, 374, 376, 377, 0, 383, 384, 374, 394, 392, + 377, 397, 395, 380, 381, 383, 408, 388, 392, 393, + 387, 389, 408, 409, 410, 398, 412, 400, 412, 404, + + 416, 400, 418, 402, 407, 408, 0, 414, 415, 405, + 425, 423, 408, 428, 426, 427, 427, 424, 425, 431, + 431, 421, 0, 418, 425, 422, 422, 437, 438, 422, + 427, 428, 442, 430, 445, 432, 447, 447, 434, 0, + 445, 440, 447, 442, 444, 0, 0, 456, 457, 456, + 444, 461, 459, 447, 464, 458, 459, 449, 454, 0, + 466, 467, 0, 0, 455, 456, 457, 472, 459, 474, + 474, 0, 471, 466, 473, 468, 0, 0, 481, 482, + 481, 469, 486, 484, 472, 489, 483, 475, 480, 481, + 0, 0, 478, 492, 494, 0, 479, 485, 486, 497, + + 499, 500, 485, 481, 506, 483, 508, 0, 491, 497, + 499, 499, 501, 520, 515, 516, 504, 494, 495, 507, + 497, 498, 510, 511, 525, 509, 513, 514, 526, 527, + 507, 532, 509, 534, 0, 522, 524, 524, 526, 539, + 540, 528, 518, 519, 531, 521, 522, 534, 0, 542, + 543, 542, 534, 552, 549, 534, 535, 539, 0, 0, + 0, 0, 540, 0, 541, 537, 541, 547, 543, 549, + 549, 547, 548, 568, 0, 0, 569, 0, 0, 564, + 565, 553, 565, 554, 555, 0, 0, 0, 559, 0, + 560, 559, 565, 561, 567, 563, 564, 584, 0, 0, + + 585, 0, 0, 586, 569, 570, 575, 596, 574, 575, + 574, 575, 577, 572, 573, 584, 595, 581, 597, 583, + 603, 584, 597, 598, 594, 595, 591, 592, 607, 598, + 594, 595, 591, 592, 613, 599, 615, 601, 613, 614, + 610, 611, 606, 0, 0, 609, 614, 604, 0, 0, + 0, 621, 0, 0, 613, 618, 624, 620, 626, 617, + 622, 623, 624, 637, 638, 0, 0, 0, 624, 0, + 0, 0, 0, 629, 635, 631, 637, 632, 633, 646, + 647, 636, 643, 652, 0, 639, 651, 655, 642, 657, + 644, 641, 643, 648, 649, 659, 660, 657, 666, 653, + + 668, 655, 657, 658, 668, 669, 657, 656, 664, 664, + 0, 665, 666, 667, 668, 660, 663, 0, 0, 0, + 0, 665, 672, 673, 674, 675, 0, 0, 0, 0, + 0, 665, 686, 0, 689, 0, 690, 0, 679, 682, + 671, 694, 0, 695, 0, 0, 0, 694, 695, 683, + 0, 0, 697, 698, 0, 0, 700, 0, 0, 0, + 1254, 717, 721, 725, 729, 728, 733, 737, 736, 741, + 745, 744, 749, 753 +}; + +static const flex_int16_t yy_def[1175] = +{ + 0, + 1161, 1, 1161, 3, 1161, 5, 1162, 1162, 1163, 1163, + 1161, 1161, 1161, 1161, 1164, 1165, 1161, 1161, 1166, 1166, + 1166, 1166, 1166, 1166, 1161, 1161, 1161, 1161, 1167, 1168, + 1161, 1161, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, + 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, + 1161, 1161, 1161, 1161, 1170, 1171, 1161, 1161, 1161, 1172, + 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, + 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1161, 1161, + 1173, 1161, 1161, 1174, 1161, 1161, 1164, 1161, 1165, 1161, + 1161, 1166, 1166, 1166, 1166, 1166, 1166, 1166, 1166, 1161, + + 1167, 1161, 1168, 1161, 1161, 1169, 1169, 1169, 1169, 1169, + 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, + 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, + 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1161, + 1170, 1161, 1171, 1161, 1161, 1172, 1172, 1172, 1172, 1172, + 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, + 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, + 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, + 1172, 1172, 1173, 1161, 1174, 1166, 1166, 1166, 1166, 1166, + 1166, 1166, 1166, 1169, 1169, 1169, 1169, 1169, 1169, 1169, + + 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, + 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, + 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, + 1169, 1169, 1169, 1172, 1172, 1172, 1172, 1172, 1172, 1172, + 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, + 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, + 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, + 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1166, 1166, + 1166, 1166, 1166, 1166, 1166, 1166, 1169, 1169, 1169, 1169, + 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, + + 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, + 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, + 1169, 1169, 1169, 1169, 1169, 1169, 1172, 1172, 1172, 1172, + 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, + 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, + 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, + 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, + 1172, 1166, 1166, 1166, 1166, 1166, 1166, 1169, 1169, 1169, + 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, + 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, + + 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, + 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, + 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, + 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, + 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, + 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, + 1172, 1172, 1172, 1172, 1172, 1166, 1166, 1166, 1166, 1166, + 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, + 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, + 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, + + 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, + 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1172, 1172, + 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, + 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, + 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, + 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, + 1172, 1172, 1166, 1166, 1166, 1166, 1166, 1166, 1169, 1169, + 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, + 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, + 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, + + 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, + 1169, 1169, 1169, 1169, 1169, 1172, 1172, 1172, 1172, 1172, + 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, + 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, + 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, + 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1166, 1166, + 1166, 1166, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, + 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, + 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, + 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, + + 1169, 1169, 1169, 1169, 1169, 1169, 1172, 1172, 1172, 1172, + 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, + 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, + 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1166, + 1166, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, + 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, + 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, + 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, + 1169, 1169, 1169, 1172, 1172, 1172, 1172, 1172, 1172, 1172, + 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, + + 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, + 1172, 1172, 1172, 1172, 1172, 1166, 1166, 1169, 1169, 1169, + 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, + 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, + 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, + 1169, 1169, 1169, 1169, 1169, 1172, 1172, 1172, 1172, 1172, + 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, + 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, + 1172, 1172, 1172, 1172, 1172, 1172, 1166, 1166, 1169, 1169, + 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, + + 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, + 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, + 1169, 1169, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, + 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, + 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1166, 1166, 1169, + 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, + 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, + 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1172, + 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, + 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, + + 1172, 1172, 1172, 1166, 1169, 1169, 1169, 1169, 1169, 1169, + 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, + 1169, 1169, 1169, 1169, 1169, 1169, 1172, 1172, 1172, 1172, + 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, + 1172, 1172, 1166, 1169, 1169, 1169, 1169, 1169, 1169, 1169, + 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, + 1169, 1169, 1169, 1169, 1169, 1172, 1172, 1172, 1172, 1172, + 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, + 1172, 1166, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, + 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1172, 1172, 1172, + + 1172, 1172, 1172, 1172, 1172, 1172, 1166, 1169, 1169, 1169, + 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, + 1169, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, 1172, + 1166, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, 1169, + 1172, 1172, 1172, 1172, 1172, 1169, 1169, 1169, 1169, 1169, + 1169, 1172, 1172, 1172, 1169, 1169, 1169, 1172, 1172, 1169, + 0, 1161, 1161, 1161, 1161, 1161, 1161, 1161, 1161, 1161, + 1161, 1161, 1161, 1161 +}; + +static const flex_int16_t yy_nxt[1295] = +{ + 0, + 12, 13, 14, 13, 15, 16, 12, 12, 17, 18, + 19, 19, 19, 19, 19, 20, 19, 19, 19, 19, + 19, 19, 19, 19, 19, 21, 22, 19, 19, 19, + 19, 23, 24, 19, 19, 19, 19, 19, 25, 12, + 26, 27, 28, 27, 29, 30, 26, 26, 31, 32, + 33, 33, 33, 34, 35, 36, 37, 38, 39, 40, + 33, 41, 42, 33, 43, 44, 45, 33, 46, 33, + 47, 48, 33, 33, 49, 50, 33, 33, 51, 52, + 26, 53, 54, 53, 55, 56, 57, 26, 58, 59, + 60, 60, 60, 61, 60, 62, 63, 64, 65, 66, + + 60, 67, 68, 69, 70, 71, 72, 60, 73, 60, + 74, 75, 76, 77, 60, 78, 60, 60, 79, 80, + 82, 83, 82, 83, 86, 90, 86, 93, 91, 97, + 94, 100, 104, 100, 107, 105, 114, 108, 111, 109, + 117, 120, 121, 123, 98, 125, 112, 127, 91, 115, + 129, 124, 113, 128, 118, 105, 133, 211, 138, 126, + 134, 140, 144, 140, 130, 145, 150, 131, 156, 212, + 135, 136, 139, 147, 157, 148, 166, 152, 149, 160, + 151, 153, 760, 168, 161, 145, 167, 154, 172, 761, + 155, 169, 173, 219, 170, 174, 177, 178, 180, 86, + + 175, 86, 220, 176, 90, 181, 182, 91, 190, 198, + 100, 191, 100, 104, 202, 199, 105, 205, 203, 224, + 140, 144, 140, 225, 145, 204, 237, 91, 239, 240, + 247, 206, 238, 244, 251, 252, 105, 256, 266, 262, + 271, 248, 319, 263, 145, 320, 328, 245, 257, 264, + 329, 333, 762, 763, 272, 267, 362, 334, 363, 392, + 399, 414, 418, 364, 393, 400, 415, 419, 436, 440, + 457, 463, 471, 437, 441, 458, 464, 472, 491, 764, + 493, 492, 416, 494, 512, 563, 515, 513, 519, 516, + 514, 459, 517, 520, 537, 539, 557, 538, 540, 558, + + 560, 575, 559, 561, 580, 564, 562, 604, 765, 581, + 624, 645, 605, 582, 766, 625, 646, 576, 693, 626, + 727, 767, 772, 694, 768, 728, 770, 769, 773, 771, + 774, 775, 776, 777, 778, 779, 780, 781, 782, 783, + 784, 785, 786, 787, 788, 789, 790, 791, 792, 793, + 794, 795, 796, 797, 798, 799, 800, 801, 803, 805, + 802, 804, 806, 807, 808, 809, 810, 811, 812, 813, + 814, 815, 816, 817, 818, 819, 820, 821, 822, 823, + 824, 825, 826, 827, 828, 829, 830, 831, 832, 833, + 834, 835, 836, 837, 838, 839, 840, 841, 842, 843, + + 844, 845, 846, 847, 848, 849, 850, 851, 852, 853, + 854, 855, 856, 857, 858, 859, 860, 861, 862, 863, + 864, 865, 866, 867, 868, 869, 870, 871, 872, 873, + 874, 875, 876, 877, 878, 879, 880, 881, 882, 883, + 884, 885, 886, 887, 888, 889, 890, 891, 892, 893, + 894, 895, 896, 897, 898, 899, 900, 901, 902, 903, + 904, 905, 906, 907, 908, 909, 910, 911, 912, 913, + 914, 915, 916, 917, 918, 919, 920, 921, 922, 923, + 924, 925, 926, 927, 928, 929, 930, 931, 932, 933, + 934, 935, 936, 937, 938, 939, 940, 941, 942, 943, + + 944, 945, 946, 947, 948, 949, 950, 951, 952, 953, + 954, 955, 956, 957, 958, 959, 960, 961, 962, 963, + 964, 965, 966, 967, 968, 969, 970, 971, 972, 973, + 974, 975, 976, 977, 978, 979, 980, 981, 982, 983, + 984, 985, 986, 987, 988, 989, 990, 991, 992, 993, + 994, 995, 996, 997, 998, 999, 1000, 1001, 1002, 1003, + 1004, 1005, 1006, 1007, 1008, 1009, 1010, 1011, 1012, 1013, + 1014, 1015, 1016, 1017, 1018, 1019, 1020, 1021, 1022, 1023, + 1024, 1025, 1026, 1027, 1028, 1029, 1030, 1031, 1032, 1033, + 1034, 1035, 1036, 1037, 1038, 1039, 1040, 1041, 1042, 1043, + + 1044, 1045, 1046, 1047, 1048, 1049, 1050, 1051, 1052, 1053, + 1054, 1055, 1056, 1057, 1058, 1059, 1060, 1061, 1062, 1063, + 1064, 1065, 1066, 1067, 1068, 1069, 1070, 1071, 1072, 1073, + 1074, 1075, 1076, 1077, 1078, 1079, 1080, 1081, 1082, 1083, + 1084, 1085, 1086, 1087, 1088, 1089, 1090, 1091, 1092, 1093, + 1094, 1095, 1096, 1097, 1098, 1099, 1100, 1101, 1102, 1103, + 1104, 1105, 1106, 1107, 1108, 1109, 1110, 1111, 1112, 1113, + 1114, 1115, 1116, 1117, 1118, 1119, 1120, 1121, 1122, 1123, + 1124, 1125, 1126, 1127, 1128, 1129, 1130, 1131, 1132, 1133, + 1134, 1135, 1136, 1137, 1138, 1139, 1140, 1141, 1142, 1143, + + 1144, 1145, 1146, 1147, 1148, 1149, 1150, 1151, 1152, 1153, + 1154, 1155, 1156, 1157, 1158, 1159, 1160, 81, 81, 81, + 81, 84, 84, 84, 84, 87, 87, 87, 87, 89, + 89, 92, 89, 101, 101, 101, 101, 103, 103, 106, + 103, 141, 141, 141, 141, 143, 143, 146, 143, 183, + 759, 758, 183, 185, 185, 757, 185, 756, 755, 754, + 753, 752, 751, 750, 749, 748, 747, 746, 745, 744, + 743, 742, 741, 740, 739, 738, 737, 736, 735, 734, + 733, 732, 731, 730, 729, 726, 725, 724, 723, 722, + 721, 720, 719, 718, 717, 716, 715, 714, 713, 712, + + 711, 710, 709, 708, 707, 706, 705, 704, 703, 702, + 701, 700, 699, 698, 697, 696, 695, 692, 691, 690, + 689, 688, 687, 686, 685, 684, 683, 682, 681, 680, + 679, 678, 677, 676, 675, 674, 673, 672, 671, 670, + 669, 668, 667, 666, 665, 664, 663, 662, 661, 660, + 659, 658, 657, 656, 655, 654, 653, 652, 651, 650, + 649, 648, 647, 644, 643, 642, 641, 640, 639, 638, + 637, 636, 635, 634, 633, 632, 631, 630, 629, 628, + 627, 623, 622, 621, 620, 619, 618, 617, 616, 615, + 614, 613, 612, 611, 610, 609, 608, 607, 606, 603, + + 602, 601, 600, 599, 598, 597, 596, 595, 594, 593, + 592, 591, 590, 589, 588, 587, 586, 585, 584, 583, + 579, 578, 577, 574, 573, 572, 571, 570, 569, 568, + 567, 566, 565, 556, 555, 554, 553, 552, 551, 550, + 549, 548, 547, 546, 545, 544, 543, 542, 541, 536, + 535, 534, 533, 532, 531, 530, 529, 528, 527, 526, + 525, 524, 523, 522, 521, 518, 511, 510, 509, 508, + 507, 506, 505, 504, 503, 502, 501, 500, 499, 498, + 497, 496, 495, 490, 489, 488, 487, 486, 485, 484, + 483, 482, 481, 480, 479, 478, 477, 476, 475, 474, + + 473, 470, 469, 468, 467, 466, 465, 462, 461, 460, + 456, 455, 454, 453, 452, 451, 450, 449, 448, 447, + 446, 445, 444, 443, 442, 439, 438, 435, 434, 433, + 432, 431, 430, 429, 428, 427, 426, 425, 424, 423, + 422, 421, 420, 417, 413, 412, 411, 410, 409, 408, + 407, 406, 405, 404, 403, 402, 401, 398, 397, 396, + 395, 394, 391, 390, 389, 388, 387, 386, 385, 384, + 383, 382, 381, 380, 379, 378, 377, 376, 375, 374, + 373, 372, 371, 370, 369, 368, 367, 366, 365, 361, + 360, 359, 358, 357, 356, 355, 354, 353, 352, 351, + + 350, 349, 348, 347, 346, 345, 344, 343, 342, 341, + 340, 339, 338, 337, 336, 335, 332, 331, 330, 327, + 326, 325, 324, 323, 322, 321, 318, 317, 316, 315, + 314, 313, 312, 311, 310, 309, 308, 307, 306, 305, + 304, 303, 302, 301, 300, 299, 298, 297, 296, 295, + 294, 293, 292, 291, 290, 289, 288, 287, 286, 285, + 284, 283, 282, 281, 280, 279, 184, 278, 277, 276, + 275, 274, 273, 270, 269, 268, 265, 261, 260, 259, + 258, 255, 254, 253, 250, 249, 246, 243, 242, 241, + 236, 235, 234, 142, 233, 232, 231, 230, 229, 228, + + 227, 226, 223, 222, 221, 218, 217, 216, 215, 214, + 213, 210, 209, 208, 207, 201, 200, 197, 196, 195, + 194, 102, 193, 192, 189, 188, 187, 186, 88, 184, + 179, 171, 165, 164, 163, 162, 159, 158, 142, 137, + 132, 122, 119, 116, 110, 102, 99, 96, 95, 88, + 1161, 85, 85, 11, 1161, 1161, 1161, 1161, 1161, 1161, + 1161, 1161, 1161, 1161, 1161, 1161, 1161, 1161, 1161, 1161, + 1161, 1161, 1161, 1161, 1161, 1161, 1161, 1161, 1161, 1161, + 1161, 1161, 1161, 1161, 1161, 1161, 1161, 1161, 1161, 1161, + 1161, 1161, 1161, 1161 +}; + +static const flex_int16_t yy_chk[1295] = +{ + 0, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 7, 7, 8, 8, 13, 17, 13, 20, 17, 23, + 20, 27, 31, 27, 34, 31, 37, 34, 36, 34, + 39, 41, 41, 43, 23, 44, 36, 45, 17, 37, + 46, 43, 36, 45, 39, 31, 48, 120, 50, 44, + 48, 53, 58, 53, 46, 58, 62, 46, 64, 120, + 48, 48, 50, 61, 64, 61, 72, 63, 61, 67, + 62, 63, 683, 73, 67, 58, 72, 63, 75, 684, + 63, 73, 75, 127, 73, 75, 76, 76, 78, 86, + + 75, 86, 127, 75, 90, 78, 78, 90, 97, 111, + 100, 97, 100, 104, 114, 111, 104, 115, 114, 131, + 140, 144, 140, 131, 144, 114, 150, 90, 151, 151, + 157, 115, 150, 155, 160, 160, 104, 165, 172, 170, + 176, 157, 226, 170, 144, 226, 236, 155, 165, 170, + 236, 240, 685, 686, 176, 172, 271, 240, 271, 301, + 307, 323, 325, 271, 301, 307, 323, 325, 343, 347, + 365, 369, 378, 343, 347, 365, 369, 378, 399, 687, + 400, 399, 323, 400, 418, 466, 419, 418, 421, 419, + 418, 365, 419, 421, 440, 441, 463, 440, 441, 463, + + 464, 477, 463, 464, 481, 466, 464, 505, 688, 481, + 528, 549, 505, 481, 689, 528, 549, 477, 601, 528, + 642, 690, 695, 601, 693, 642, 694, 693, 696, 694, + 697, 698, 699, 700, 701, 702, 703, 704, 705, 706, + 707, 708, 709, 710, 711, 713, 714, 715, 717, 718, + 719, 720, 721, 722, 723, 724, 725, 727, 728, 729, + 727, 728, 730, 731, 732, 733, 734, 735, 736, 737, + 738, 739, 740, 741, 742, 743, 744, 745, 746, 747, + 748, 749, 750, 751, 752, 753, 754, 755, 756, 758, + 761, 762, 763, 764, 765, 766, 767, 768, 769, 770, + + 771, 772, 773, 774, 776, 777, 778, 779, 780, 781, + 782, 783, 784, 785, 786, 787, 788, 789, 790, 791, + 792, 793, 794, 795, 796, 797, 798, 799, 800, 801, + 802, 803, 804, 805, 806, 808, 809, 810, 811, 812, + 813, 814, 815, 816, 817, 818, 819, 820, 821, 822, + 824, 825, 826, 827, 828, 829, 830, 831, 832, 833, + 834, 835, 836, 837, 838, 839, 841, 842, 843, 844, + 845, 848, 849, 850, 851, 852, 853, 854, 855, 856, + 857, 858, 859, 861, 862, 865, 866, 867, 868, 869, + 870, 871, 873, 874, 875, 876, 879, 880, 881, 882, + + 883, 884, 885, 886, 887, 888, 889, 890, 893, 894, + 895, 897, 898, 899, 900, 901, 902, 903, 904, 905, + 906, 907, 909, 910, 911, 912, 913, 914, 915, 916, + 917, 918, 919, 920, 921, 922, 923, 924, 925, 926, + 927, 928, 929, 930, 931, 932, 933, 934, 936, 937, + 938, 939, 940, 941, 942, 943, 944, 945, 946, 947, + 948, 950, 951, 952, 953, 954, 955, 956, 957, 958, + 963, 965, 966, 967, 968, 969, 970, 971, 971, 972, + 973, 974, 977, 980, 981, 982, 983, 984, 985, 989, + 991, 992, 993, 994, 995, 996, 997, 998, 1001, 1004, + + 1005, 1006, 1007, 1008, 1009, 1010, 1011, 1012, 1013, 1014, + 1015, 1016, 1017, 1018, 1019, 1020, 1021, 1022, 1023, 1024, + 1025, 1026, 1027, 1028, 1029, 1030, 1031, 1032, 1033, 1034, + 1035, 1036, 1037, 1038, 1039, 1040, 1041, 1042, 1043, 1046, + 1047, 1048, 1052, 1055, 1056, 1057, 1058, 1059, 1060, 1061, + 1062, 1063, 1064, 1065, 1069, 1074, 1075, 1076, 1077, 1078, + 1079, 1080, 1081, 1082, 1083, 1084, 1086, 1087, 1088, 1089, + 1090, 1091, 1092, 1093, 1094, 1095, 1096, 1097, 1098, 1099, + 1100, 1101, 1102, 1103, 1104, 1105, 1106, 1107, 1108, 1109, + 1110, 1112, 1113, 1114, 1115, 1116, 1117, 1122, 1123, 1124, + + 1125, 1126, 1132, 1133, 1135, 1137, 1139, 1140, 1141, 1142, + 1144, 1148, 1149, 1150, 1153, 1154, 1157, 1162, 1162, 1162, + 1162, 1163, 1163, 1163, 1163, 1164, 1164, 1164, 1164, 1165, + 1165, 1166, 1165, 1167, 1167, 1167, 1167, 1168, 1168, 1169, + 1168, 1170, 1170, 1170, 1170, 1171, 1171, 1172, 1171, 1173, + 682, 681, 1173, 1174, 1174, 680, 1174, 679, 678, 677, + 675, 674, 672, 671, 670, 669, 668, 667, 666, 665, + 664, 663, 660, 659, 658, 657, 656, 655, 654, 653, + 649, 648, 647, 646, 645, 641, 640, 637, 636, 635, + 634, 633, 632, 631, 630, 628, 627, 625, 624, 623, + + 620, 619, 618, 617, 616, 615, 614, 613, 612, 611, + 610, 608, 607, 606, 605, 604, 603, 600, 599, 598, + 597, 596, 595, 594, 593, 592, 591, 590, 589, 588, + 587, 586, 585, 583, 581, 580, 579, 578, 577, 576, + 575, 574, 573, 572, 571, 570, 569, 568, 567, 564, + 563, 562, 561, 560, 559, 558, 557, 556, 555, 554, + 553, 552, 550, 548, 547, 546, 545, 544, 543, 542, + 541, 540, 539, 538, 537, 535, 534, 532, 531, 530, + 529, 527, 526, 525, 524, 523, 522, 520, 519, 517, + 516, 515, 514, 513, 512, 510, 509, 508, 506, 504, + + 503, 502, 501, 500, 499, 498, 497, 494, 493, 492, + 491, 490, 489, 488, 487, 486, 485, 484, 483, 482, + 480, 479, 478, 476, 475, 474, 473, 472, 471, 470, + 469, 468, 467, 461, 460, 459, 458, 457, 452, 451, + 450, 449, 448, 447, 446, 445, 444, 443, 442, 439, + 437, 436, 435, 434, 432, 431, 430, 429, 428, 427, + 426, 425, 424, 423, 422, 420, 417, 416, 415, 414, + 413, 412, 411, 410, 409, 408, 407, 406, 405, 404, + 403, 402, 401, 398, 397, 396, 394, 393, 392, 391, + 390, 389, 388, 387, 386, 385, 384, 383, 382, 381, + + 380, 377, 375, 374, 373, 372, 370, 368, 367, 366, + 364, 363, 362, 361, 360, 359, 358, 357, 356, 355, + 354, 353, 352, 351, 350, 346, 344, 342, 341, 340, + 339, 338, 336, 335, 334, 333, 332, 331, 330, 329, + 328, 327, 326, 324, 322, 321, 320, 319, 318, 317, + 315, 314, 312, 311, 310, 309, 308, 306, 305, 304, + 303, 302, 300, 299, 298, 297, 296, 295, 294, 293, + 292, 291, 290, 289, 288, 287, 286, 284, 283, 281, + 280, 279, 278, 277, 276, 275, 274, 273, 272, 269, + 268, 266, 265, 264, 263, 262, 261, 260, 258, 257, + + 256, 255, 254, 253, 252, 251, 250, 249, 248, 247, + 246, 245, 244, 243, 242, 241, 239, 238, 237, 235, + 233, 232, 231, 230, 228, 227, 225, 224, 223, 222, + 221, 220, 219, 218, 217, 216, 215, 214, 213, 212, + 211, 210, 209, 208, 207, 206, 205, 204, 203, 202, + 201, 200, 199, 198, 197, 196, 195, 194, 193, 192, + 191, 190, 189, 188, 187, 186, 184, 182, 181, 180, + 179, 178, 177, 175, 174, 173, 171, 169, 168, 167, + 166, 164, 163, 162, 159, 158, 156, 154, 153, 152, + 149, 148, 147, 141, 139, 138, 137, 136, 135, 134, + + 133, 132, 130, 129, 128, 126, 125, 124, 123, 122, + 121, 119, 118, 117, 116, 113, 112, 110, 109, 108, + 107, 101, 99, 98, 96, 95, 94, 93, 87, 82, + 77, 74, 71, 70, 69, 68, 66, 65, 55, 49, + 47, 42, 40, 38, 35, 29, 24, 22, 21, 15, + 11, 10, 9, 1161, 1161, 1161, 1161, 1161, 1161, 1161, + 1161, 1161, 1161, 1161, 1161, 1161, 1161, 1161, 1161, 1161, + 1161, 1161, 1161, 1161, 1161, 1161, 1161, 1161, 1161, 1161, + 1161, 1161, 1161, 1161, 1161, 1161, 1161, 1161, 1161, 1161, + 1161, 1161, 1161, 1161 +}; + +static yy_state_type yy_last_accepting_state; +static char *yy_last_accepting_cpos; + +extern int yy_flex_debug; +int yy_flex_debug = 0; + +/* The intent behind this definition is that it'll catch + * any uses of REJECT which flex missed. + */ +#define REJECT reject_used_but_not_detected +#define yymore() yymore_used_but_not_detected +#define YY_MORE_ADJ 0 +#define YY_RESTORE_YY_MORE_OFFSET +char *yytext; +#line 1 "test_spec_scan.l" +#line 2 "test_spec_scan.l" + +/* + * src/bin/pgaftest/test_spec_scan.l + * Flex lexer for .pgaf test specification files. + * + * The outer structure (cluster, setup, teardown, step, sequence) is + * tokenised in the INITIAL state. When a step/setup/teardown body + * opens, we switch to the STEP_BODY exclusive state where every + * command keyword and value becomes its own token. + * + * Additional exclusive states: + * CLUSTER_BODY - inside a cluster { } block; tokenises the cluster + * mini-language (formations, nodes, options) + * EXEC_ARGS - skip whitespace, then read the service name token + * EXEC_ARGS_REST - read the rest of the line as a raw T_SHELL_ARGS string + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the PostgreSQL License. + */ + +#include +#include "test_spec.h" +#include "test_spec_parse.h" + +int pgaf_line_number = 1; + +/* + * When the lexer sees "setup", "teardown", or "step", it sets this flag + * so the next "{" enters STEP_BODY rather than doing the raw block read. + */ +static int pgaf_next_brace_is_step = 0; + +/* + * When the lexer sees "cluster", it sets this flag so the next "{" + * enters CLUSTER_BODY rather than doing the raw block read. + */ +static int pgaf_next_brace_is_cluster = 0; + +/* + * When 1, the next '{' in STEP_BODY returns T_LBRACE (for a while body) + * instead of calling pgaf_read_raw_block(). Set by the grammar via a + * mid-rule action when T_WHILE is seen. Also used by the setup/teardown + * blocks that need sub-block parsing. + */ +int pgaf_next_brace_is_while = 0; + +/* + * Nesting depth for '{' tokens returned as T_LBRACE inside STEP_BODY + * (while bodies). 0 means no nested while bodies are open. Each T_LBRACE + * increments this; each T_RBRACE decrements it when depth > 0; when depth + * reaches 0 the '}' is the step's own closing brace → BEGIN(INITIAL). + */ +static int pgaf_step_brace_depth = 0; + +/* + * Nesting depth inside CLUSTER_BODY - so we know when the outermost "}" + * closes and we can return to INITIAL. + */ +static int pgaf_cluster_depth = 0; + +static char * +pgaf_strdup(const char *s) +{ + char *r = strdup(s); + if (!r) + { + fprintf(stderr, "out of memory\n"); + exit(1); + } + return r; +} + + +/* + * pgaf_read_raw_block: called when '{' is seen and we want to collect + * everything up to the matching '}' as a single string. Used for + * sql/expect content inside step bodies. + * Stores a freshly allocated trimmed string in yylval.str. + * + * Defined in the user-code section (after the second %%) so that it can + * call flex's static input() function. + */ +static void pgaf_read_raw_block(void); +#line 1192 "test_spec_scan.c" + +#line 1194 "test_spec_scan.c" + +#define INITIAL 0 +#define CLUSTER_BODY 1 +#define STEP_BODY 2 +#define EXEC_ARGS 3 +#define EXEC_ARGS_REST 4 + +#ifndef YY_NO_UNISTD_H + +/* Special case for "unistd.h", since it is non-ANSI. We include it way + * down here because we want the user's section 1 to have been scanned first. + * The user has a chance to override it with an option. + */ +#include +#endif + +#ifndef YY_EXTRA_TYPE +#define YY_EXTRA_TYPE void * +#endif + +static int yy_init_globals(void); + +/* Accessor methods to globals. + * These are made visible to non-reentrant scanners for convenience. */ + +int yylex_destroy(void); + +int yyget_debug(void); + +void yyset_debug(int debug_flag); + +YY_EXTRA_TYPE yyget_extra(void); + +void yyset_extra(YY_EXTRA_TYPE user_defined); + +FILE * yyget_in(void); + +void yyset_in(FILE *_in_str); + +FILE * yyget_out(void); + +void yyset_out(FILE *_out_str); + +yy_size_t yyget_leng(void); + +char * yyget_text(void); + +int yyget_lineno(void); + +void yyset_lineno(int _line_number); + +/* Macros after this point can all be overridden by user definitions in + * section 1. + */ + +#ifndef YY_SKIP_YYWRAP +#ifdef __cplusplus +extern "C" int yywrap(void); +#else +extern int yywrap(void); +#endif +#endif + +#ifndef YY_NO_UNPUT + +#endif + +#ifndef yytext_ptr +static void yy_flex_strncpy(char *, const char *, int); +#endif + +#ifdef YY_NEED_STRLEN +static int yy_flex_strlen(const char *); +#endif + +#ifndef YY_NO_INPUT +#ifdef __cplusplus +static int yyinput(void); +#else +static int input(void); +#endif + +#endif + +/* Amount of stuff to slurp up with each read. */ +#ifndef YY_READ_BUF_SIZE +#ifdef __ia64__ + +/* On IA-64, the buffer size is 16k, not 8k */ +#define YY_READ_BUF_SIZE 16384 +#else +#define YY_READ_BUF_SIZE 8192 +#endif /* __ia64__ */ +#endif + +/* Copy whatever the last rule matched to the standard output. */ +#ifndef ECHO + +/* This used to be an fputs(), but since the string might contain NUL's, + * we now use fwrite(). + */ +#define ECHO do { if (fwrite(yytext, (size_t) yyleng, 1, yyout)) { } \ +} while (0) +#endif + +/* Gets input and stuffs it into "buf". number of characters read, or YY_NULL, + * is returned in "result". + */ +#ifndef YY_INPUT +#define YY_INPUT(buf, result, max_size) \ + if (YY_CURRENT_BUFFER_LVALUE->yy_is_interactive) \ + { \ + int c = '*'; \ + yy_size_t n; \ + for (n = 0; n < max_size && \ + (c = getc(yyin)) != EOF && c != '\n'; ++n) { \ + buf[n] = (char) c; } \ + if (c == '\n') { \ + buf[n++] = (char) c; } \ + if (c == EOF && ferror(yyin)) { \ + YY_FATAL_ERROR("input in flex scanner failed"); } \ + result = n; \ + } \ + else \ + { \ + errno = 0; \ + while ((result = (int) fread(buf, 1, (yy_size_t) max_size, yyin)) == 0 && ferror( \ + yyin)) \ + { \ + if (errno != EINTR) \ + { \ + YY_FATAL_ERROR("input in flex scanner failed"); \ + break; \ + } \ + errno = 0; \ + clearerr(yyin); \ + } \ + } \ + \ + +#endif + +/* No semi-colon after return; correct usage is to write "yyterminate();" - + * we don't want an extra ';' after the "return" because that will cause + * some compilers to complain about unreachable statements. + */ +#ifndef yyterminate +#define yyterminate() return YY_NULL +#endif + +/* Number of entries by which start-condition stack grows. */ +#ifndef YY_START_STACK_INCR +#define YY_START_STACK_INCR 25 +#endif + +/* Report a fatal error. */ +#ifndef YY_FATAL_ERROR +#define YY_FATAL_ERROR(msg) yy_fatal_error(msg) +#endif + +/* end tables serialization structures and prototypes */ + +/* Default declaration of generated scanner - a define so the user can + * easily add parameters. + */ +#ifndef YY_DECL +#define YY_DECL_IS_OURS 1 + +extern int yylex(void); + +#define YY_DECL int yylex(void) +#endif /* !YY_DECL */ + +/* Code executed at the beginning of each rule, after yytext and yyleng + * have been set up. + */ +#ifndef YY_USER_ACTION +#define YY_USER_ACTION +#endif + +/* Code executed at the end of each rule. */ +#ifndef YY_BREAK +#define YY_BREAK /*LINTED*/ break; +#endif + +#define YY_RULE_SETUP \ + YY_USER_ACTION + +/** The main scanner function which does all the work. + */ +YY_DECL +{ + yy_state_type yy_current_state; + char *yy_cp, *yy_bp; + int yy_act; + + if (!(yy_init)) + { + (yy_init) = 1; + +#ifdef YY_USER_INIT + YY_USER_INIT; +#endif + + if (!(yy_start)) + { + (yy_start) = 1; /* first start state */ + } + if (!yyin) + { + yyin = stdin; + } + + if (!yyout) + { + yyout = stdout; + } + + if (!YY_CURRENT_BUFFER) + { + yyensure_buffer_stack(); + YY_CURRENT_BUFFER_LVALUE = + yy_create_buffer(yyin, YY_BUF_SIZE); + } + + yy_load_buffer_state(); + } + + { +#line 89 "test_spec_scan.l" + + +#line 1416 "test_spec_scan.c" + + while (/*CONSTCOND*/ 1) /* loops until end-of-file is reached */ + { + yy_cp = (yy_c_buf_p); + + /* Support of yytext. */ + *yy_cp = (yy_hold_char); + + /* yy_bp points to the position in yy_ch_buf of the start of + * the current run. + */ + yy_bp = yy_cp; + + yy_current_state = (yy_start); +yy_match: + do { + YY_CHAR yy_c = yy_ec[YY_SC_TO_UI(*yy_cp)]; + if (yy_accept[yy_current_state]) + { + (yy_last_accepting_state) = yy_current_state; + (yy_last_accepting_cpos) = yy_cp; + } + while (yy_chk[yy_base[yy_current_state] + yy_c] != yy_current_state) + { + yy_current_state = (int) yy_def[yy_current_state]; + if (yy_current_state >= 1162) + { + yy_c = yy_meta[yy_c]; + } + } + yy_current_state = yy_nxt[yy_base[yy_current_state] + yy_c]; + ++yy_cp; + } while (yy_current_state != 1161); + yy_cp = (yy_last_accepting_cpos); + yy_current_state = (yy_last_accepting_state); + +yy_find_action: + yy_act = yy_accept[yy_current_state]; + + YY_DO_BEFORE_ACTION; + +do_action: /* This label is used only to access EOF actions. */ + + switch (yy_act) + { /* beginning of action switch */ + case 0: /* must back up */ + /* undo the effects of YY_DO_BEFORE_ACTION */ + *yy_cp = (yy_hold_char); + yy_cp = (yy_last_accepting_cpos); + yy_current_state = (yy_last_accepting_state); + goto yy_find_action; + + case 1: + YY_RULE_SETUP +#line 91 "test_spec_scan.l" + { /* comment */ + } + + YY_BREAK + case 2: + YY_RULE_SETUP +#line 93 "test_spec_scan.l" + { + pgaf_next_brace_is_cluster = 1; + return T_CLUSTER; + } + + YY_BREAK + case 3: + YY_RULE_SETUP +#line 94 "test_spec_scan.l" + { + return T_MONITOR; + } + + YY_BREAK + case 4: + YY_RULE_SETUP +#line 95 "test_spec_scan.l" + { + return T_NODE; + } + + YY_BREAK + case 5: + YY_RULE_SETUP +#line 96 "test_spec_scan.l" + { + return T_CITUS_COORDINATOR; + } + + YY_BREAK + case 6: + YY_RULE_SETUP +#line 97 "test_spec_scan.l" + { + return T_CITUS_WORKER; + } + + YY_BREAK + case 7: + YY_RULE_SETUP +#line 99 "test_spec_scan.l" + { + pgaf_next_brace_is_step = 1; + return T_SETUP; + } + + YY_BREAK + case 8: + YY_RULE_SETUP +#line 100 "test_spec_scan.l" + { + pgaf_next_brace_is_step = 1; + return T_TEARDOWN; + } + + YY_BREAK + case 9: + YY_RULE_SETUP +#line 101 "test_spec_scan.l" + { + pgaf_next_brace_is_step = 1; + return T_STEP; + } + + YY_BREAK + case 10: + YY_RULE_SETUP +#line 102 "test_spec_scan.l" + { + return T_SEQUENCE; + } + + YY_BREAK + case 11: + YY_RULE_SETUP +#line 104 "test_spec_scan.l" + { + return T_EQUALS; + } + + YY_BREAK + case 12: + YY_RULE_SETUP +#line 106 "test_spec_scan.l" + { + yylval.ival = atoi(yytext); + return T_INTEGER; + } + + YY_BREAK + case 13: + YY_RULE_SETUP +#line 111 "test_spec_scan.l" + { + yylval.str = pgaf_strdup(yytext); + return T_IDENT; + } + + YY_BREAK + case 14: + +/* rule 14 can match eol */ + YY_RULE_SETUP +#line 116 "test_spec_scan.l" + { + yytext[yyleng - 1] = '\0'; + yylval.str = pgaf_strdup(yytext + 1); + return T_STRING; + } + + YY_BREAK + case 15: + YY_RULE_SETUP +#line 122 "test_spec_scan.l" + { + if (pgaf_next_brace_is_step) + { + pgaf_next_brace_is_step = 0; + BEGIN(STEP_BODY); + return T_LBRACE; + } + if (pgaf_next_brace_is_cluster) + { + pgaf_next_brace_is_cluster = 0; + pgaf_cluster_depth = 1; + BEGIN(CLUSTER_BODY); + return T_LBRACE; + } + + /* fallback: should not happen in a well-formed file */ + fprintf(stderr, "pgaftest: unexpected '{' at line %d\n", + pgaf_line_number); + } + + YY_BREAK + case 16: + +/* rule 16 can match eol */ + YY_RULE_SETUP +#line 140 "test_spec_scan.l" + { + pgaf_line_number++; + } + + YY_BREAK + case 17: + YY_RULE_SETUP +#line 141 "test_spec_scan.l" + { /* whitespace */ + } + + YY_BREAK + case 18: + YY_RULE_SETUP +#line 143 "test_spec_scan.l" + { + fprintf(stderr, + "pgaftest: unexpected character '%c' at line %d\n", + yytext[0], pgaf_line_number); + } + + YY_BREAK + case 19: + YY_RULE_SETUP +#line 148 "test_spec_scan.l" + { /* comment */ + } + + YY_BREAK + case 20: + +/* rule 20 can match eol */ + YY_RULE_SETUP +#line 149 "test_spec_scan.l" + { + pgaf_line_number++; + } + + YY_BREAK + case 21: + YY_RULE_SETUP +#line 150 "test_spec_scan.l" + { /* whitespace */ + } + + YY_BREAK + case 22: + YY_RULE_SETUP +#line 152 "test_spec_scan.l" + { + return T_MONITOR; + } + + YY_BREAK + case 23: + YY_RULE_SETUP +#line 153 "test_spec_scan.l" + { + return T_IMAGE_TARGET; + } + + YY_BREAK + case 24: + YY_RULE_SETUP +#line 154 "test_spec_scan.l" + { + return T_IMAGE; + } + + YY_BREAK + case 25: + YY_RULE_SETUP +#line 155 "test_spec_scan.l" + { + return T_SSL; + } + + YY_BREAK + case 26: + YY_RULE_SETUP +#line 156 "test_spec_scan.l" + { + return T_AUTH_METHOD; + } + + YY_BREAK + case 27: + YY_RULE_SETUP +#line 157 "test_spec_scan.l" + { + return T_AUTH; + } + + YY_BREAK + case 28: + YY_RULE_SETUP +#line 158 "test_spec_scan.l" + { + return T_FORMATION; + } + + YY_BREAK + case 29: + YY_RULE_SETUP +#line 159 "test_spec_scan.l" + { + return T_NUM_SYNC; + } + + YY_BREAK + case 30: + YY_RULE_SETUP +#line 160 "test_spec_scan.l" + { + return T_NODE; + } + + YY_BREAK + case 31: + YY_RULE_SETUP +#line 162 "test_spec_scan.l" + { + return T_COORDINATOR; + } + + YY_BREAK + case 32: + YY_RULE_SETUP +#line 163 "test_spec_scan.l" + { + return T_WORKER; + } + + YY_BREAK + case 33: + YY_RULE_SETUP +#line 164 "test_spec_scan.l" + { + return T_ASYNC; + } + + YY_BREAK + case 34: + YY_RULE_SETUP +#line 165 "test_spec_scan.l" + { + return T_NO_MONITOR; + } + + YY_BREAK + case 35: + YY_RULE_SETUP +#line 166 "test_spec_scan.l" + { + return T_PASSWORD; + } + + YY_BREAK + case 36: + YY_RULE_SETUP +#line 167 "test_spec_scan.l" + { + return T_MONITOR_PASSWORD; + } + + YY_BREAK + case 37: + YY_RULE_SETUP +#line 168 "test_spec_scan.l" + { + return T_LAUNCH; + } + + YY_BREAK + case 38: + YY_RULE_SETUP +#line 169 "test_spec_scan.l" + { + return T_DEFERRED; + } + + YY_BREAK + case 39: + YY_RULE_SETUP +#line 170 "test_spec_scan.l" + { + return T_IMMEDIATE; + } + + YY_BREAK + case 40: + YY_RULE_SETUP +#line 171 "test_spec_scan.l" + { + return T_INITIALLY; + } + + YY_BREAK + case 41: + YY_RULE_SETUP +#line 172 "test_spec_scan.l" + { + return T_STOPPED; + } + + YY_BREAK + case 42: + YY_RULE_SETUP +#line 173 "test_spec_scan.l" + { + return T_VOLUME; + } + + YY_BREAK + case 43: + YY_RULE_SETUP +#line 174 "test_spec_scan.l" + { + return T_LISTEN; + } + + YY_BREAK + case 44: + YY_RULE_SETUP +#line 175 "test_spec_scan.l" + { + return T_CITUS_SECONDARY; + } + + YY_BREAK + case 45: + YY_RULE_SETUP +#line 176 "test_spec_scan.l" + { + return T_CANDIDATE_PRIORITY; + } + + YY_BREAK + case 46: + YY_RULE_SETUP +#line 177 "test_spec_scan.l" + { + return T_GROUP; + } + + YY_BREAK + case 47: + YY_RULE_SETUP +#line 178 "test_spec_scan.l" + { + return T_PORT; + } + + YY_BREAK + case 48: + YY_RULE_SETUP +#line 179 "test_spec_scan.l" + { + return T_CITUS_CLUSTER_NAME; + } + + YY_BREAK + case 49: + YY_RULE_SETUP +#line 180 "test_spec_scan.l" + { + return T_DEBIAN_CLUSTER; + } + + YY_BREAK + case 50: + YY_RULE_SETUP +#line 181 "test_spec_scan.l" + { + return T_REPLICATION_QUORUM; + } + + YY_BREAK + case 51: + YY_RULE_SETUP +#line 182 "test_spec_scan.l" + { + return T_REPLICATION_PASSWORD; + } + + YY_BREAK + case 52: + YY_RULE_SETUP +#line 183 "test_spec_scan.l" + { + return T_EXTENSION_VERSION; + } + + YY_BREAK + case 53: + YY_RULE_SETUP +#line 184 "test_spec_scan.l" + { + return T_BIND_SOURCE; + } + + YY_BREAK + case 54: + YY_RULE_SETUP +#line 186 "test_spec_scan.l" + { + return T_EQUALS; + } + + YY_BREAK + case 55: + YY_RULE_SETUP +#line 188 "test_spec_scan.l" + { + yylval.ival = atoi(yytext); + return T_INTEGER; + } + + YY_BREAK + case 56: + +/* rule 56 can match eol */ + YY_RULE_SETUP +#line 193 "test_spec_scan.l" + { + yytext[yyleng - 1] = '\0'; + yylval.str = pgaf_strdup(yytext + 1); + return T_STRING; + } + + YY_BREAK + case 57: + YY_RULE_SETUP +#line 199 "test_spec_scan.l" + { + pgaf_cluster_depth++; + return T_LBRACE; + } + + YY_BREAK + case 58: + YY_RULE_SETUP +#line 204 "test_spec_scan.l" + { + pgaf_cluster_depth--; + if (pgaf_cluster_depth == 0) + { + BEGIN(INITIAL); + } + return T_RBRACE; + } + + YY_BREAK + case 59: + YY_RULE_SETUP +#line 211 "test_spec_scan.l" + { + return T_FS_INIT; + } + + YY_BREAK + case 60: + YY_RULE_SETUP +#line 212 "test_spec_scan.l" + { + return T_FS_SINGLE; + } + + YY_BREAK + case 61: + YY_RULE_SETUP +#line 213 "test_spec_scan.l" + { + return T_FS_PRIMARY; + } + + YY_BREAK + case 62: + YY_RULE_SETUP +#line 214 "test_spec_scan.l" + { + return T_FS_WAIT_PRIMARY; + } + + YY_BREAK + case 63: + YY_RULE_SETUP +#line 215 "test_spec_scan.l" + { + return T_FS_WAIT_PRIMARY; + } + + YY_BREAK + case 64: + YY_RULE_SETUP +#line 216 "test_spec_scan.l" + { + return T_FS_WAIT_STANDBY; + } + + YY_BREAK + case 65: + YY_RULE_SETUP +#line 217 "test_spec_scan.l" + { + return T_FS_WAIT_STANDBY; + } + + YY_BREAK + case 66: + YY_RULE_SETUP +#line 218 "test_spec_scan.l" + { + return T_FS_DEMOTED; + } + + YY_BREAK + case 67: + YY_RULE_SETUP +#line 219 "test_spec_scan.l" + { + return T_FS_DEMOTE_TIMEOUT; + } + + YY_BREAK + case 68: + YY_RULE_SETUP +#line 220 "test_spec_scan.l" + { + return T_FS_DEMOTE_TIMEOUT; + } + + YY_BREAK + case 69: + YY_RULE_SETUP +#line 221 "test_spec_scan.l" + { + return T_FS_DRAINING; + } + + YY_BREAK + case 70: + YY_RULE_SETUP +#line 222 "test_spec_scan.l" + { + return T_FS_SECONDARY; + } + + YY_BREAK + case 71: + YY_RULE_SETUP +#line 223 "test_spec_scan.l" + { + return T_FS_CATCHINGUP; + } + + YY_BREAK + case 72: + YY_RULE_SETUP +#line 224 "test_spec_scan.l" + { + return T_FS_PREP_PROMOTION; + } + + YY_BREAK + case 73: + YY_RULE_SETUP +#line 225 "test_spec_scan.l" + { + return T_FS_PREP_PROMOTION; + } + + YY_BREAK + case 74: + YY_RULE_SETUP +#line 226 "test_spec_scan.l" + { + return T_FS_STOP_REPLICATION; + } + + YY_BREAK + case 75: + YY_RULE_SETUP +#line 227 "test_spec_scan.l" + { + return T_FS_STOP_REPLICATION; + } + + YY_BREAK + case 76: + YY_RULE_SETUP +#line 228 "test_spec_scan.l" + { + return T_FS_MAINTENANCE; + } + + YY_BREAK + case 77: + YY_RULE_SETUP +#line 229 "test_spec_scan.l" + { + return T_FS_JOIN_PRIMARY; + } + + YY_BREAK + case 78: + YY_RULE_SETUP +#line 230 "test_spec_scan.l" + { + return T_FS_JOIN_PRIMARY; + } + + YY_BREAK + case 79: + YY_RULE_SETUP +#line 231 "test_spec_scan.l" + { + return T_FS_APPLY_SETTINGS; + } + + YY_BREAK + case 80: + YY_RULE_SETUP +#line 232 "test_spec_scan.l" + { + return T_FS_APPLY_SETTINGS; + } + + YY_BREAK + case 81: + YY_RULE_SETUP +#line 233 "test_spec_scan.l" + { + return T_FS_PREPARE_MAINTENANCE; + } + + YY_BREAK + case 82: + YY_RULE_SETUP +#line 234 "test_spec_scan.l" + { + return T_FS_PREPARE_MAINTENANCE; + } + + YY_BREAK + case 83: + YY_RULE_SETUP +#line 235 "test_spec_scan.l" + { + return T_FS_WAIT_MAINTENANCE; + } + + YY_BREAK + case 84: + YY_RULE_SETUP +#line 236 "test_spec_scan.l" + { + return T_FS_WAIT_MAINTENANCE; + } + + YY_BREAK + case 85: + YY_RULE_SETUP +#line 237 "test_spec_scan.l" + { + return T_FS_REPORT_LSN; + } + + YY_BREAK + case 86: + YY_RULE_SETUP +#line 238 "test_spec_scan.l" + { + return T_FS_REPORT_LSN; + } + + YY_BREAK + case 87: + YY_RULE_SETUP +#line 239 "test_spec_scan.l" + { + return T_FS_FAST_FORWARD; + } + + YY_BREAK + case 88: + YY_RULE_SETUP +#line 240 "test_spec_scan.l" + { + return T_FS_FAST_FORWARD; + } + + YY_BREAK + case 89: + YY_RULE_SETUP +#line 241 "test_spec_scan.l" + { + return T_FS_JOIN_SECONDARY; + } + + YY_BREAK + case 90: + YY_RULE_SETUP +#line 242 "test_spec_scan.l" + { + return T_FS_JOIN_SECONDARY; + } + + YY_BREAK + case 91: + YY_RULE_SETUP +#line 243 "test_spec_scan.l" + { + return T_FS_DROPPED; + } + + YY_BREAK + case 92: + YY_RULE_SETUP +#line 245 "test_spec_scan.l" + { + yylval.str = pgaf_strdup(yytext); + return T_IDENT; + } + + YY_BREAK + case 93: + YY_RULE_SETUP +#line 250 "test_spec_scan.l" + { /* comment */ + } + + YY_BREAK + case 94: + +/* rule 94 can match eol */ + YY_RULE_SETUP +#line 251 "test_spec_scan.l" + { + pgaf_line_number++; + } + + YY_BREAK + case 95: + YY_RULE_SETUP +#line 252 "test_spec_scan.l" + { /* whitespace */ + } + + YY_BREAK + case 96: + YY_RULE_SETUP +#line 254 "test_spec_scan.l" + { + BEGIN(EXEC_ARGS); + return T_EXEC_FAILS; + } + + YY_BREAK + case 97: + YY_RULE_SETUP +#line 255 "test_spec_scan.l" + { + BEGIN(EXEC_ARGS); + return T_EXEC; + } + + YY_BREAK + case 98: + YY_RULE_SETUP +#line 256 "test_spec_scan.l" + { + BEGIN(EXEC_ARGS); + return T_PG_AUTOCTL; + } + + YY_BREAK + case 99: + YY_RULE_SETUP +#line 258 "test_spec_scan.l" + { + return T_WAIT; + } + + YY_BREAK + case 100: + YY_RULE_SETUP +#line 259 "test_spec_scan.l" + { + return T_UNTIL; + } + + YY_BREAK + case 101: + YY_RULE_SETUP +#line 260 "test_spec_scan.l" + { + return T_TIMEOUT; + } + + YY_BREAK + case 102: + YY_RULE_SETUP +#line 261 "test_spec_scan.l" + { + return T_ASSERT; + } + + YY_BREAK + case 103: + YY_RULE_SETUP +#line 262 "test_spec_scan.l" + { + return T_SQL; + } + + YY_BREAK + case 104: + YY_RULE_SETUP +#line 263 "test_spec_scan.l" + { + return T_EXPECT; + } + + YY_BREAK + case 105: + YY_RULE_SETUP +#line 264 "test_spec_scan.l" + { + return T_ERROR; + } + + YY_BREAK + case 106: + YY_RULE_SETUP +#line 265 "test_spec_scan.l" + { + return T_PROMOTE; + } + + YY_BREAK + case 107: + YY_RULE_SETUP +#line 266 "test_spec_scan.l" + { + return T_NETWORK; + } + + YY_BREAK + case 108: + YY_RULE_SETUP +#line 267 "test_spec_scan.l" + { + return T_DISCONNECT; + } + + YY_BREAK + case 109: + YY_RULE_SETUP +#line 268 "test_spec_scan.l" + { + return T_CONNECT; + } + + YY_BREAK + case 110: + YY_RULE_SETUP +#line 269 "test_spec_scan.l" + { + return T_SLEEP; + } + + YY_BREAK + case 111: + YY_RULE_SETUP +#line 270 "test_spec_scan.l" + { + return T_COMPOSE; + } + + YY_BREAK + case 112: + YY_RULE_SETUP +#line 271 "test_spec_scan.l" + { + return T_DOWN; + } + + YY_BREAK + case 113: + YY_RULE_SETUP +#line 272 "test_spec_scan.l" + { + return T_START; + } + + YY_BREAK + case 114: + YY_RULE_SETUP +#line 273 "test_spec_scan.l" + { + return T_STOP; + } + + YY_BREAK + case 115: + YY_RULE_SETUP +#line 274 "test_spec_scan.l" + { + return T_STOPPED; + } + + YY_BREAK + case 116: + YY_RULE_SETUP +#line 275 "test_spec_scan.l" + { + return T_KILL; + } + + YY_BREAK + case 117: + YY_RULE_SETUP +#line 276 "test_spec_scan.l" + { + return T_IN; + } + + YY_BREAK + case 118: + YY_RULE_SETUP +#line 277 "test_spec_scan.l" + { + return T_STATE; + } + + YY_BREAK + case 119: + YY_RULE_SETUP +#line 278 "test_spec_scan.l" + { + return T_ASSIGNED_STATE; + } + + YY_BREAK + case 120: + YY_RULE_SETUP +#line 279 "test_spec_scan.l" + { + return T_CANDIDATE_PRIORITY; + } + + YY_BREAK + case 121: + YY_RULE_SETUP +#line 280 "test_spec_scan.l" + { + return T_GROUP; + } + + YY_BREAK + case 122: + YY_RULE_SETUP +#line 281 "test_spec_scan.l" + { + return T_AND; + } + + YY_BREAK + case 123: + YY_RULE_SETUP +#line 282 "test_spec_scan.l" + { + return T_IS; + } + + YY_BREAK + case 124: + YY_RULE_SETUP +#line 283 "test_spec_scan.l" + { + return T_WITH; + } + + YY_BREAK + case 125: + YY_RULE_SETUP +#line 284 "test_spec_scan.l" + { + return T_EQUALS; + } + + YY_BREAK + case 126: + YY_RULE_SETUP +#line 285 "test_spec_scan.l" + { + return T_COMMA; + } + + YY_BREAK + case 127: + YY_RULE_SETUP +#line 286 "test_spec_scan.l" + { + return T_POSTGRES; + } + + YY_BREAK + case 128: + YY_RULE_SETUP +#line 287 "test_spec_scan.l" + { + return T_STAYS; + } + + YY_BREAK + case 129: + YY_RULE_SETUP +#line 288 "test_spec_scan.l" + { + return T_WHILE; + } + + YY_BREAK + case 130: + YY_RULE_SETUP +#line 289 "test_spec_scan.l" + { + return T_THROUGH; + } + + YY_BREAK + case 131: + YY_RULE_SETUP +#line 290 "test_spec_scan.l" + { + return T_SET; + } + + YY_BREAK + case 132: + YY_RULE_SETUP +#line 291 "test_spec_scan.l" + { + BEGIN(EXEC_ARGS); + return T_INJECT; + } + + YY_BREAK + case 133: + YY_RULE_SETUP +#line 292 "test_spec_scan.l" + { + return T_LOGS; + } + + YY_BREAK + case 134: + YY_RULE_SETUP +#line 293 "test_spec_scan.l" + { + return T_NOT; + } + + YY_BREAK + case 135: + YY_RULE_SETUP +#line 294 "test_spec_scan.l" + { + return T_CONTAINS; + } + + YY_BREAK + case 136: + YY_RULE_SETUP +#line 295 "test_spec_scan.l" + { + return T_MATCHES; + } + + YY_BREAK + case 137: + YY_RULE_SETUP +#line 297 "test_spec_scan.l" + { + yylval.ival = atoi(yytext); + return T_INTEGER; + } + + YY_BREAK + case 138: + +/* rule 138 can match eol */ + YY_RULE_SETUP +#line 302 "test_spec_scan.l" + { + yytext[yyleng - 1] = '\0'; + yylval.str = pgaf_strdup(yytext + 1); + return T_STRING; + } + + YY_BREAK + case 139: + YY_RULE_SETUP +#line 308 "test_spec_scan.l" + { + if (pgaf_next_brace_is_while) + { + pgaf_next_brace_is_while = 0; + pgaf_step_brace_depth++; + return T_LBRACE; + } + pgaf_read_raw_block(); + return T_BLOCK; + } + + YY_BREAK + case 140: + YY_RULE_SETUP +#line 318 "test_spec_scan.l" + { + if (pgaf_step_brace_depth > 0) + { + pgaf_step_brace_depth--; + return T_RBRACE; + } + pgaf_step_brace_depth = 0; + BEGIN(INITIAL); + return T_RBRACE; + } + + YY_BREAK + case 141: + YY_RULE_SETUP +#line 328 "test_spec_scan.l" + { + yylval.str = pgaf_strdup(yytext); + return T_IDENT; + } + + YY_BREAK + case 142: + YY_RULE_SETUP +#line 333 "test_spec_scan.l" + { /* skip whitespace before service name */ + } + + YY_BREAK + case 143: + YY_RULE_SETUP +#line 335 "test_spec_scan.l" + { + yylval.str = pgaf_strdup(yytext); + BEGIN(EXEC_ARGS_REST); + return T_IDENT; + } + + YY_BREAK + case 144: + +/* rule 144 can match eol */ + YY_RULE_SETUP +#line 341 "test_spec_scan.l" + { + pgaf_line_number++; + BEGIN(STEP_BODY); + } + + YY_BREAK + case 145: + YY_RULE_SETUP +#line 346 "test_spec_scan.l" + { + char *p = yytext; + while (*p == ' ' || *p == '\t') + { + p++; + } + yylval.str = pgaf_strdup(p); + BEGIN(STEP_BODY); + return T_SHELL_ARGS; + } + + YY_BREAK + case 146: + +/* rule 146 can match eol */ + YY_RULE_SETUP +#line 354 "test_spec_scan.l" + { + pgaf_line_number++; + BEGIN(STEP_BODY); + } + + YY_BREAK + case 147: + YY_RULE_SETUP +#line 359 "test_spec_scan.l" + ECHO; + + YY_BREAK +#line 2301 "test_spec_scan.c" + case YY_STATE_EOF(INITIAL): + case YY_STATE_EOF(CLUSTER_BODY): + case YY_STATE_EOF(STEP_BODY): + case YY_STATE_EOF(EXEC_ARGS): + case YY_STATE_EOF(EXEC_ARGS_REST): + yyterminate(); + + case YY_END_OF_BUFFER: + { + /* Amount of text matched not including the EOB char. */ + int yy_amount_of_matched_text = (int) (yy_cp - (yytext_ptr)) - 1; + + /* Undo the effects of YY_DO_BEFORE_ACTION. */ + *yy_cp = (yy_hold_char); + YY_RESTORE_YY_MORE_OFFSET + + if (YY_CURRENT_BUFFER_LVALUE->yy_buffer_status == YY_BUFFER_NEW) + { + /* We're scanning a new file or input source. It's + * possible that this happened because the user + * just pointed yyin at a new source and called + * yylex(). If so, then we have to assure + * consistency between YY_CURRENT_BUFFER and our + * globals. Here is the right place to do so, because + * this is the first action (other than possibly a + * back-up) that will match for the new input source. + */ + (yy_n_chars) = YY_CURRENT_BUFFER_LVALUE->yy_n_chars; + YY_CURRENT_BUFFER_LVALUE->yy_input_file = yyin; + YY_CURRENT_BUFFER_LVALUE->yy_buffer_status = YY_BUFFER_NORMAL; + } + + /* Note that here we test for yy_c_buf_p "<=" to the position + * of the first EOB in the buffer, since yy_c_buf_p will + * already have been incremented past the NUL character + * (since all states make transitions on EOB to the + * end-of-buffer state). Contrast this with the test + * in input(). + */ + if ((yy_c_buf_p) <= + &YY_CURRENT_BUFFER_LVALUE->yy_ch_buf[(yy_n_chars)]) + { /* This was really a NUL. */ + yy_state_type yy_next_state; + + (yy_c_buf_p) = (yytext_ptr) + yy_amount_of_matched_text; + + yy_current_state = yy_get_previous_state(); + + /* Okay, we're now positioned to make the NUL + * transition. We couldn't have + * yy_get_previous_state() go ahead and do it + * for us because it doesn't know how to deal + * with the possibility of jamming (and we don't + * want to build jamming into it because then it + * will run more slowly). + */ + + yy_next_state = yy_try_NUL_trans(yy_current_state); + + yy_bp = (yytext_ptr) + YY_MORE_ADJ; + + if (yy_next_state) + { + /* Consume the NUL. */ + yy_cp = ++(yy_c_buf_p); + yy_current_state = yy_next_state; + goto yy_match; + } + else + { + yy_cp = (yy_last_accepting_cpos); + yy_current_state = (yy_last_accepting_state); + goto yy_find_action; + } + } + else + { + switch (yy_get_next_buffer()) + { + case EOB_ACT_END_OF_FILE: + { + (yy_did_buffer_switch_on_eof) = 0; + + if (yywrap()) + { + /* Note: because we've taken care in + * yy_get_next_buffer() to have set up + * yytext, we can now set up + * yy_c_buf_p so that if some total + * hoser (like flex itself) wants to + * call the scanner after we return the + * YY_NULL, it'll still work - another + * YY_NULL will get returned. + */ + (yy_c_buf_p) = (yytext_ptr) + YY_MORE_ADJ; + + yy_act = YY_STATE_EOF(YY_START); + goto do_action; + } + else + { + if (!(yy_did_buffer_switch_on_eof)) + { + YY_NEW_FILE; + } + } + break; + } + + case EOB_ACT_CONTINUE_SCAN: + (yy_c_buf_p) = + (yytext_ptr) + yy_amount_of_matched_text; + + yy_current_state = yy_get_previous_state(); + + yy_cp = (yy_c_buf_p); + yy_bp = (yytext_ptr) + YY_MORE_ADJ; + goto yy_match; + + case EOB_ACT_LAST_MATCH: + (yy_c_buf_p) = + &YY_CURRENT_BUFFER_LVALUE->yy_ch_buf[(yy_n_chars)]; + + yy_current_state = yy_get_previous_state(); + + yy_cp = (yy_c_buf_p); + yy_bp = (yytext_ptr) + YY_MORE_ADJ; + goto yy_find_action; + } + } + break; + } + + default: + YY_FATAL_ERROR( + "fatal flex scanner internal error--no action found"); + } /* end of action switch */ + } /* end of scanning one token */ + } /* end of user's declarations */ +} /* end of yylex */ + +/* yy_get_next_buffer - try to read in a new buffer + * + * Returns a code representing an action: + * EOB_ACT_LAST_MATCH - + * EOB_ACT_CONTINUE_SCAN - continue scanning from current position + * EOB_ACT_END_OF_FILE - end of file + */ +static int +yy_get_next_buffer(void) +{ + char *dest = YY_CURRENT_BUFFER_LVALUE->yy_ch_buf; + char *source = (yytext_ptr); + int number_to_move, i; + int ret_val; + + if ((yy_c_buf_p) > &YY_CURRENT_BUFFER_LVALUE->yy_ch_buf[(yy_n_chars) + 1]) + { + YY_FATAL_ERROR( + "fatal flex scanner internal error--end of buffer missed"); + } + + if (YY_CURRENT_BUFFER_LVALUE->yy_fill_buffer == 0) + { /* Don't try to fill the buffer, so this is an EOF. */ + if ((yy_c_buf_p) - (yytext_ptr) - YY_MORE_ADJ == 1) + { + /* We matched a single character, the EOB, so + * treat this as a final EOF. + */ + return EOB_ACT_END_OF_FILE; + } + else + { + /* We matched some text prior to the EOB, first + * process it. + */ + return EOB_ACT_LAST_MATCH; + } + } + + /* Try to read more data. */ + + /* First move last chars to start of buffer. */ + number_to_move = (int) ((yy_c_buf_p) - (yytext_ptr) - 1); + + for (i = 0; i < number_to_move; ++i) + { + *(dest++) = *(source++); + } + + if (YY_CURRENT_BUFFER_LVALUE->yy_buffer_status == YY_BUFFER_EOF_PENDING) + { + /* don't do the read, it's not guaranteed to return an EOF, + * just force an EOF + */ + YY_CURRENT_BUFFER_LVALUE->yy_n_chars = (yy_n_chars) = 0; + } + else + { + yy_size_t num_to_read = + YY_CURRENT_BUFFER_LVALUE->yy_buf_size - number_to_move - 1; + + while (num_to_read <= 0) + { /* Not enough room in the buffer - grow it. */ + /* just a shorter name for the current buffer */ + YY_BUFFER_STATE b = YY_CURRENT_BUFFER_LVALUE; + + int yy_c_buf_p_offset = + (int) ((yy_c_buf_p) - b->yy_ch_buf); + + if (b->yy_is_our_buffer) + { + yy_size_t new_size = b->yy_buf_size * 2; + + if (new_size <= 0) + { + b->yy_buf_size += b->yy_buf_size / 8; + } + else + { + b->yy_buf_size *= 2; + } + + b->yy_ch_buf = (char *) + + /* Include room in for 2 EOB chars. */ + yyrealloc((void *) b->yy_ch_buf, + (yy_size_t) (b->yy_buf_size + 2)); + } + else + { + /* Can't grow it, we don't own it. */ + b->yy_ch_buf = NULL; + } + + if (!b->yy_ch_buf) + { + YY_FATAL_ERROR( + "fatal error - scanner input buffer overflow"); + } + + (yy_c_buf_p) = &b->yy_ch_buf[yy_c_buf_p_offset]; + + num_to_read = YY_CURRENT_BUFFER_LVALUE->yy_buf_size - + number_to_move - 1; + } + + if (num_to_read > YY_READ_BUF_SIZE) + { + num_to_read = YY_READ_BUF_SIZE; + } + + /* Read in more data. */ + YY_INPUT((&YY_CURRENT_BUFFER_LVALUE->yy_ch_buf[number_to_move]), + (yy_n_chars), num_to_read); + + YY_CURRENT_BUFFER_LVALUE->yy_n_chars = (yy_n_chars); + } + + if ((yy_n_chars) == 0) + { + if (number_to_move == YY_MORE_ADJ) + { + ret_val = EOB_ACT_END_OF_FILE; + yyrestart(yyin); + } + else + { + ret_val = EOB_ACT_LAST_MATCH; + YY_CURRENT_BUFFER_LVALUE->yy_buffer_status = + YY_BUFFER_EOF_PENDING; + } + } + else + { + ret_val = EOB_ACT_CONTINUE_SCAN; + } + + if (((yy_n_chars) + number_to_move) > YY_CURRENT_BUFFER_LVALUE->yy_buf_size) + { + /* Extend the array by 50%, plus the number we really need. */ + yy_size_t new_size = (yy_n_chars) + number_to_move + ((yy_n_chars) >> 1); + YY_CURRENT_BUFFER_LVALUE->yy_ch_buf = (char *) yyrealloc( + (void *) YY_CURRENT_BUFFER_LVALUE->yy_ch_buf, (yy_size_t) new_size); + if (!YY_CURRENT_BUFFER_LVALUE->yy_ch_buf) + { + YY_FATAL_ERROR("out of dynamic memory in yy_get_next_buffer()"); + } + + /* "- 2" to take care of EOB's */ + YY_CURRENT_BUFFER_LVALUE->yy_buf_size = (int) (new_size - 2); + } + + (yy_n_chars) += number_to_move; + YY_CURRENT_BUFFER_LVALUE->yy_ch_buf[(yy_n_chars)] = YY_END_OF_BUFFER_CHAR; + YY_CURRENT_BUFFER_LVALUE->yy_ch_buf[(yy_n_chars) + 1] = YY_END_OF_BUFFER_CHAR; + + (yytext_ptr) = &YY_CURRENT_BUFFER_LVALUE->yy_ch_buf[0]; + + return ret_val; +} + + +/* yy_get_previous_state - get the state just before the EOB char was reached */ + +static yy_state_type +yy_get_previous_state(void) +{ + yy_state_type yy_current_state; + char *yy_cp; + + yy_current_state = (yy_start); + + for (yy_cp = (yytext_ptr) + YY_MORE_ADJ; yy_cp < (yy_c_buf_p); ++yy_cp) + { + YY_CHAR yy_c = (*yy_cp ? yy_ec[YY_SC_TO_UI(*yy_cp)] : 1); + if (yy_accept[yy_current_state]) + { + (yy_last_accepting_state) = yy_current_state; + (yy_last_accepting_cpos) = yy_cp; + } + while (yy_chk[yy_base[yy_current_state] + yy_c] != yy_current_state) + { + yy_current_state = (int) yy_def[yy_current_state]; + if (yy_current_state >= 1162) + { + yy_c = yy_meta[yy_c]; + } + } + yy_current_state = yy_nxt[yy_base[yy_current_state] + yy_c]; + } + + return yy_current_state; +} + + +/* yy_try_NUL_trans - try to make a transition on the NUL character + * + * synopsis + * next_state = yy_try_NUL_trans( current_state ); + */ +static yy_state_type +yy_try_NUL_trans(yy_state_type yy_current_state) +{ + int yy_is_jam; + char *yy_cp = (yy_c_buf_p); + + YY_CHAR yy_c = 1; + if (yy_accept[yy_current_state]) + { + (yy_last_accepting_state) = yy_current_state; + (yy_last_accepting_cpos) = yy_cp; + } + while (yy_chk[yy_base[yy_current_state] + yy_c] != yy_current_state) + { + yy_current_state = (int) yy_def[yy_current_state]; + if (yy_current_state >= 1162) + { + yy_c = yy_meta[yy_c]; + } + } + yy_current_state = yy_nxt[yy_base[yy_current_state] + yy_c]; + yy_is_jam = (yy_current_state == 1161); + + return yy_is_jam ? 0 : yy_current_state; +} + + +#ifndef YY_NO_UNPUT + +#endif + +#ifndef YY_NO_INPUT +#ifdef __cplusplus +static int +yyinput(void) +#else +static int +input(void) +#endif +{ + int c; + + *(yy_c_buf_p) = (yy_hold_char); + + if (*(yy_c_buf_p) == YY_END_OF_BUFFER_CHAR) + { + /* yy_c_buf_p now points to the character we want to return. + * If this occurs *before* the EOB characters, then it's a + * valid NUL; if not, then we've hit the end of the buffer. + */ + if ((yy_c_buf_p) < &YY_CURRENT_BUFFER_LVALUE->yy_ch_buf[(yy_n_chars)]) + { + /* This was really a NUL. */ + *(yy_c_buf_p) = '\0'; + } + else + { /* need more input */ + yy_size_t offset = (yy_c_buf_p) - (yytext_ptr); + ++(yy_c_buf_p); + + switch (yy_get_next_buffer()) + { + case EOB_ACT_LAST_MATCH: + + /* This happens because yy_g_n_b() + * sees that we've accumulated a + * token and flags that we need to + * try matching the token before + * proceeding. But for input(), + * there's no matching to consider. + * So convert the EOB_ACT_LAST_MATCH + * to EOB_ACT_END_OF_FILE. + */ + + /* Reset buffer status. */ + yyrestart(yyin); + + /*FALLTHROUGH*/ + + case EOB_ACT_END_OF_FILE: + { + if (yywrap()) + { + return 0; + } + + if (!(yy_did_buffer_switch_on_eof)) + { + YY_NEW_FILE; + } +#ifdef __cplusplus + return yyinput(); +#else + return input(); +#endif + } + + case EOB_ACT_CONTINUE_SCAN: + (yy_c_buf_p) = (yytext_ptr) + offset; + break; + } + } + } + + c = *(unsigned char *) (yy_c_buf_p); /* cast for 8-bit char's */ + *(yy_c_buf_p) = '\0'; /* preserve yytext */ + (yy_hold_char) = *++(yy_c_buf_p); + + return c; +} + + +#endif /* ifndef YY_NO_INPUT */ + +/** Immediately switch to a different input stream. + * @param input_file A readable stream. + * + * @note This function does not reset the start condition to @c INITIAL . + */ +void +yyrestart(FILE *input_file) +{ + if (!YY_CURRENT_BUFFER) + { + yyensure_buffer_stack(); + YY_CURRENT_BUFFER_LVALUE = + yy_create_buffer(yyin, YY_BUF_SIZE); + } + + yy_init_buffer(YY_CURRENT_BUFFER, input_file); + yy_load_buffer_state(); +} + + +/** Switch to a different input buffer. + * @param new_buffer The new input buffer. + * + */ +void +yy_switch_to_buffer(YY_BUFFER_STATE new_buffer) +{ + /* TODO. We should be able to replace this entire function body + * with + * yypop_buffer_state(); + * yypush_buffer_state(new_buffer); + */ + yyensure_buffer_stack(); + if (YY_CURRENT_BUFFER == new_buffer) + { + return; + } + + if (YY_CURRENT_BUFFER) + { + /* Flush out information for old buffer. */ + *(yy_c_buf_p) = (yy_hold_char); + YY_CURRENT_BUFFER_LVALUE->yy_buf_pos = (yy_c_buf_p); + YY_CURRENT_BUFFER_LVALUE->yy_n_chars = (yy_n_chars); + } + + YY_CURRENT_BUFFER_LVALUE = new_buffer; + yy_load_buffer_state(); + + /* We don't actually know whether we did this switch during + * EOF (yywrap()) processing, but the only time this flag + * is looked at is after yywrap() is called, so it's safe + * to go ahead and always set it. + */ + (yy_did_buffer_switch_on_eof) = 1; +} + + +static void +yy_load_buffer_state(void) +{ + (yy_n_chars) = YY_CURRENT_BUFFER_LVALUE->yy_n_chars; + (yytext_ptr) = (yy_c_buf_p) = YY_CURRENT_BUFFER_LVALUE->yy_buf_pos; + yyin = YY_CURRENT_BUFFER_LVALUE->yy_input_file; + (yy_hold_char) = *(yy_c_buf_p); +} + + +/** Allocate and initialize an input buffer state. + * @param file A readable stream. + * @param size The character buffer size in bytes. When in doubt, use @c YY_BUF_SIZE. + * + * @return the allocated buffer state. + */ +YY_BUFFER_STATE +yy_create_buffer(FILE *file, int size) +{ + YY_BUFFER_STATE b; + + b = (YY_BUFFER_STATE) yyalloc(sizeof(struct yy_buffer_state)); + if (!b) + { + YY_FATAL_ERROR("out of dynamic memory in yy_create_buffer()"); + } + + b->yy_buf_size = size; + + /* yy_ch_buf has to be 2 characters longer than the size given because + * we need to put in 2 end-of-buffer characters. + */ + b->yy_ch_buf = (char *) yyalloc((yy_size_t) (b->yy_buf_size + 2)); + if (!b->yy_ch_buf) + { + YY_FATAL_ERROR("out of dynamic memory in yy_create_buffer()"); + } + + b->yy_is_our_buffer = 1; + + yy_init_buffer(b, file); + + return b; +} + + +/** Destroy the buffer. + * @param b a buffer created with yy_create_buffer() + * + */ +void +yy_delete_buffer(YY_BUFFER_STATE b) +{ + if (!b) + { + return; + } + + if (b == YY_CURRENT_BUFFER) /* Not sure if we should pop here. */ + { + YY_CURRENT_BUFFER_LVALUE = (YY_BUFFER_STATE) 0; + } + + if (b->yy_is_our_buffer) + { + yyfree((void *) b->yy_ch_buf); + } + + yyfree((void *) b); +} + + +/* Initializes or reinitializes a buffer. + * This function is sometimes called more than once on the same buffer, + * such as during a yyrestart() or at EOF. + */ +static void +yy_init_buffer(YY_BUFFER_STATE b, FILE *file) +{ + int oerrno = errno; + + yy_flush_buffer(b); + + b->yy_input_file = file; + b->yy_fill_buffer = 1; + + /* If b is the current buffer, then yy_init_buffer was _probably_ + * called from yyrestart() or through yy_get_next_buffer. + * In that case, we don't want to reset the lineno or column. + */ + if (b != YY_CURRENT_BUFFER) + { + b->yy_bs_lineno = 1; + b->yy_bs_column = 0; + } + + b->yy_is_interactive = 0; + + errno = oerrno; +} + + +/** Discard all buffered characters. On the next scan, YY_INPUT will be called. + * @param b the buffer state to be flushed, usually @c YY_CURRENT_BUFFER. + * + */ +void +yy_flush_buffer(YY_BUFFER_STATE b) +{ + if (!b) + { + return; + } + + b->yy_n_chars = 0; + + /* We always need two end-of-buffer characters. The first causes + * a transition to the end-of-buffer state. The second causes + * a jam in that state. + */ + b->yy_ch_buf[0] = YY_END_OF_BUFFER_CHAR; + b->yy_ch_buf[1] = YY_END_OF_BUFFER_CHAR; + + b->yy_buf_pos = &b->yy_ch_buf[0]; + + b->yy_at_bol = 1; + b->yy_buffer_status = YY_BUFFER_NEW; + + if (b == YY_CURRENT_BUFFER) + { + yy_load_buffer_state(); + } +} + + +/** Pushes the new state onto the stack. The new state becomes + * the current state. This function will allocate the stack + * if necessary. + * @param new_buffer The new state. + * + */ +void +yypush_buffer_state(YY_BUFFER_STATE new_buffer) +{ + if (new_buffer == NULL) + { + return; + } + + yyensure_buffer_stack(); + + /* This block is copied from yy_switch_to_buffer. */ + if (YY_CURRENT_BUFFER) + { + /* Flush out information for old buffer. */ + *(yy_c_buf_p) = (yy_hold_char); + YY_CURRENT_BUFFER_LVALUE->yy_buf_pos = (yy_c_buf_p); + YY_CURRENT_BUFFER_LVALUE->yy_n_chars = (yy_n_chars); + } + + /* Only push if top exists. Otherwise, replace top. */ + if (YY_CURRENT_BUFFER) + { + (yy_buffer_stack_top)++; + } + YY_CURRENT_BUFFER_LVALUE = new_buffer; + + /* copied from yy_switch_to_buffer. */ + yy_load_buffer_state(); + (yy_did_buffer_switch_on_eof) = 1; +} + + +/** Removes and deletes the top of the stack, if present. + * The next element becomes the new top. + * + */ +void +yypop_buffer_state(void) +{ + if (!YY_CURRENT_BUFFER) + { + return; + } + + yy_delete_buffer(YY_CURRENT_BUFFER); + YY_CURRENT_BUFFER_LVALUE = NULL; + if ((yy_buffer_stack_top) > 0) + { + --(yy_buffer_stack_top); + } + + if (YY_CURRENT_BUFFER) + { + yy_load_buffer_state(); + (yy_did_buffer_switch_on_eof) = 1; + } +} + + +/* Allocates the stack if it does not exist. + * Guarantees space for at least one push. + */ +static void +yyensure_buffer_stack(void) +{ + yy_size_t num_to_alloc; + + if (!(yy_buffer_stack)) + { + /* First allocation is just for 2 elements, since we don't know if this + * scanner will even need a stack. We use 2 instead of 1 to avoid an + * immediate realloc on the next call. + */ + num_to_alloc = 1; /* After all that talk, this was set to 1 anyways... */ + (yy_buffer_stack) = (struct yy_buffer_state **) yyalloc + (num_to_alloc * sizeof(struct yy_buffer_state *) + ); + if (!(yy_buffer_stack)) + { + YY_FATAL_ERROR("out of dynamic memory in yyensure_buffer_stack()"); + } + + memset((yy_buffer_stack), 0, num_to_alloc * sizeof(struct yy_buffer_state *)); + + (yy_buffer_stack_max) = num_to_alloc; + (yy_buffer_stack_top) = 0; + return; + } + + if ((yy_buffer_stack_top) >= ((yy_buffer_stack_max)) - 1) + { + /* Increase the buffer to prepare for a possible push. */ + yy_size_t grow_size = 8 /* arbitrary grow size */; + + num_to_alloc = (yy_buffer_stack_max) + grow_size; + (yy_buffer_stack) = (struct yy_buffer_state **) yyrealloc + ((yy_buffer_stack), + num_to_alloc * sizeof(struct yy_buffer_state *) + ); + if (!(yy_buffer_stack)) + { + YY_FATAL_ERROR("out of dynamic memory in yyensure_buffer_stack()"); + } + + /* zero only the new slots.*/ + memset((yy_buffer_stack) + (yy_buffer_stack_max), 0, grow_size * sizeof(struct + yy_buffer_state + *)); + (yy_buffer_stack_max) = num_to_alloc; + } +} + + +/** Setup the input buffer state to scan directly from a user-specified character buffer. + * @param base the character buffer + * @param size the size in bytes of the character buffer + * + * @return the newly allocated buffer state object. + */ +YY_BUFFER_STATE +yy_scan_buffer(char *base, yy_size_t size) +{ + YY_BUFFER_STATE b; + + if (size < 2 || + base[size - 2] != YY_END_OF_BUFFER_CHAR || + base[size - 1] != YY_END_OF_BUFFER_CHAR) + { + /* They forgot to leave room for the EOB's. */ + return NULL; + } + + b = (YY_BUFFER_STATE) yyalloc(sizeof(struct yy_buffer_state)); + if (!b) + { + YY_FATAL_ERROR("out of dynamic memory in yy_scan_buffer()"); + } + + b->yy_buf_size = (int) (size - 2); /* "- 2" to take care of EOB's */ + b->yy_buf_pos = b->yy_ch_buf = base; + b->yy_is_our_buffer = 0; + b->yy_input_file = NULL; + b->yy_n_chars = b->yy_buf_size; + b->yy_is_interactive = 0; + b->yy_at_bol = 1; + b->yy_fill_buffer = 0; + b->yy_buffer_status = YY_BUFFER_NEW; + + yy_switch_to_buffer(b); + + return b; +} + + +/** Setup the input buffer state to scan a string. The next call to yylex() will + * scan from a @e copy of @a str. + * @param yystr a NUL-terminated string to scan + * + * @return the newly allocated buffer state object. + * @note If you want to scan bytes that may contain NUL values, then use + * yy_scan_bytes() instead. + */ +YY_BUFFER_STATE +yy_scan_string(const char *yystr) +{ + return yy_scan_bytes(yystr, (int) strlen(yystr)); +} + + +/** Setup the input buffer state to scan the given bytes. The next call to yylex() will + * scan from a @e copy of @a bytes. + * @param yybytes the byte buffer to scan + * @param _yybytes_len the number of bytes in the buffer pointed to by @a bytes. + * + * @return the newly allocated buffer state object. + */ +YY_BUFFER_STATE +yy_scan_bytes(const char *yybytes, yy_size_t _yybytes_len) +{ + YY_BUFFER_STATE b; + char *buf; + yy_size_t n; + yy_size_t i; + + /* Get memory for full buffer, including space for trailing EOB's. */ + n = (yy_size_t) (_yybytes_len + 2); + buf = (char *) yyalloc(n); + if (!buf) + { + YY_FATAL_ERROR("out of dynamic memory in yy_scan_bytes()"); + } + + for (i = 0; i < _yybytes_len; ++i) + { + buf[i] = yybytes[i]; + } + + buf[_yybytes_len] = buf[_yybytes_len + 1] = YY_END_OF_BUFFER_CHAR; + + b = yy_scan_buffer(buf, n); + if (!b) + { + YY_FATAL_ERROR("bad buffer in yy_scan_bytes()"); + } + + /* It's okay to grow etc. this buffer, and we should throw it + * away when we're done. + */ + b->yy_is_our_buffer = 1; + + return b; +} + + +#ifndef YY_EXIT_FAILURE +#define YY_EXIT_FAILURE 2 +#endif + +static void yynoreturn +yy_fatal_error(const char *msg) +{ + fprintf(stderr, "%s\n", msg); + exit(YY_EXIT_FAILURE); +} + + +/* Redefine yyless() so it works in section 3 code. */ + +#undef yyless +#define yyless(n) \ + do \ + { \ + /* Undo effects of setting up yytext. */ \ + yy_size_t yyless_macro_arg = (n); \ + YY_LESS_LINENO(yyless_macro_arg); \ + yytext[yyleng] = (yy_hold_char); \ + (yy_c_buf_p) = yytext + yyless_macro_arg; \ + (yy_hold_char) = *(yy_c_buf_p); \ + *(yy_c_buf_p) = '\0'; \ + yyleng = yyless_macro_arg; \ + } \ + while (0) + +/* Accessor methods (get/set functions) to struct members. */ + +/** Get the current line number. + * + */ +int +yyget_lineno(void) +{ + return yylineno; +} + + +/** Get the input stream. + * + */ +FILE * +yyget_in(void) +{ + return yyin; +} + + +/** Get the output stream. + * + */ +FILE * +yyget_out(void) +{ + return yyout; +} + + +/** Get the length of the current token. + * + */ +yy_size_t +yyget_leng(void) +{ + return yyleng; +} + + +/** Get the current token. + * + */ +char * +yyget_text(void) +{ + return yytext; +} + + +/** Set the current line number. + * @param _line_number line number + * + */ +void +yyset_lineno(int _line_number) +{ + yylineno = _line_number; +} + + +/** Set the input stream. This does not discard the current + * input buffer. + * @param _in_str A readable stream. + * + * @see yy_switch_to_buffer + */ +void +yyset_in(FILE *_in_str) +{ + yyin = _in_str; +} + + +void +yyset_out(FILE *_out_str) +{ + yyout = _out_str; +} + + +int +yyget_debug(void) +{ + return yy_flex_debug; +} + + +void +yyset_debug(int _bdebug) +{ + yy_flex_debug = _bdebug; +} + + +static int +yy_init_globals(void) +{ + /* Initialization is the same as for the non-reentrant scanner. + * This function is called from yylex_destroy(), so don't allocate here. + */ + + (yy_buffer_stack) = NULL; + (yy_buffer_stack_top) = 0; + (yy_buffer_stack_max) = 0; + (yy_c_buf_p) = NULL; + (yy_init) = 0; + (yy_start) = 0; + +/* Defined in main.c */ +#ifdef YY_STDINIT + yyin = stdin; + yyout = stdout; +#else + yyin = NULL; + yyout = NULL; +#endif + + /* For future reference: Set errno on error, since we are called by + * yylex_init() + */ + return 0; +} + + +/* yylex_destroy is for both reentrant and non-reentrant scanners. */ +int +yylex_destroy(void) +{ + /* Pop the buffer stack, destroying each element. */ + while (YY_CURRENT_BUFFER) + { + yy_delete_buffer(YY_CURRENT_BUFFER); + YY_CURRENT_BUFFER_LVALUE = NULL; + yypop_buffer_state(); + } + + /* Destroy the stack itself. */ + yyfree((yy_buffer_stack)); + (yy_buffer_stack) = NULL; + + /* Reset the globals. This is important in a non-reentrant scanner so the next time + * yylex() is called, initialization will occur. */ + yy_init_globals(); + + return 0; +} + + +/* + * Internal utility routines. + */ + +#ifndef yytext_ptr +static void +yy_flex_strncpy(char *s1, const char *s2, int n) +{ + int i; + for (i = 0; i < n; ++i) + { + s1[i] = s2[i]; + } +} + + +#endif + +#ifdef YY_NEED_STRLEN +static int +yy_flex_strlen(const char *s) +{ + int n; + for (n = 0; s[n]; ++n) + { } + + return n; +} + + +#endif + +void * +yyalloc(yy_size_t size) +{ + return malloc(size); +} + + +void * +yyrealloc(void *ptr, yy_size_t size) +{ + /* The cast to (char *) in the following accommodates both + * implementations that use char* generic pointers, and those + * that use void* generic pointers. It works with the latter + * because both ANSI C and C++ allow castless assignment from + * any pointer type to void*, and deal with argument conversions + * as though doing an assignment. + */ + return realloc(ptr, size); +} + + +void +yyfree(void *ptr) +{ + free((char *) ptr); /* see yyrealloc() for (char *) cast */ +} + + +#define YYTABLES_NAME "yytables" + +#line 359 "test_spec_scan.l" + + +static void +pgaf_read_raw_block(void) +{ + int depth = 1; + int c; + char buf[65536]; + int pos = 0; + + while (depth > 0 && (c = input()) != EOF) + { + if (c == '{') + { + depth++; + } + else if (c == '}') + { + depth--; + if (depth == 0) + { + break; + } + } + else if (c == '\n') + { + pgaf_line_number++; + } + if (depth > 0 && pos < (int) sizeof(buf) - 1) + { + buf[pos++] = (char) c; + } + } + buf[pos] = '\0'; + + char *start = buf; + while (*start == ' ' || *start == '\t' || *start == '\n' || *start == '\r') + { + start++; + } + char *end = start + strlen(start); + while (end > start && + (end[-1] == ' ' || end[-1] == '\t' || + end[-1] == '\n' || end[-1] == '\r')) + { + end--; + } + *end = '\0'; + + yylval.str = pgaf_strdup(start); +} diff --git a/src/bin/pgaftest/test_spec_scan.l b/src/bin/pgaftest/test_spec_scan.l new file mode 100644 index 000000000..0ee5846d2 --- /dev/null +++ b/src/bin/pgaftest/test_spec_scan.l @@ -0,0 +1,390 @@ +%{ +/* + * src/bin/pgaftest/test_spec_scan.l + * Flex lexer for .pgaf test specification files. + * + * The outer structure (cluster, setup, teardown, step, sequence) is + * tokenised in the INITIAL state. When a step/setup/teardown body + * opens, we switch to the STEP_BODY exclusive state where every + * command keyword and value becomes its own token. + * + * Additional exclusive states: + * CLUSTER_BODY - inside a cluster { } block; tokenises the cluster + * mini-language (formations, nodes, options) + * EXEC_ARGS - skip whitespace, then read the service name token + * EXEC_ARGS_REST - read the rest of the line as a raw T_SHELL_ARGS string + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the PostgreSQL License. + */ + +#include +#include "test_spec.h" +#include "test_spec_parse.h" + +int pgaf_line_number = 1; + +/* + * When the lexer sees "setup", "teardown", or "step", it sets this flag + * so the next "{" enters STEP_BODY rather than doing the raw block read. + */ +static int pgaf_next_brace_is_step = 0; + +/* + * When the lexer sees "cluster", it sets this flag so the next "{" + * enters CLUSTER_BODY rather than doing the raw block read. + */ +static int pgaf_next_brace_is_cluster = 0; + +/* + * When 1, the next '{' in STEP_BODY returns T_LBRACE (for a while body) + * instead of calling pgaf_read_raw_block(). Set by the grammar via a + * mid-rule action when T_WHILE is seen. Also used by the setup/teardown + * blocks that need sub-block parsing. + */ +int pgaf_next_brace_is_while = 0; + +/* + * Nesting depth for '{' tokens returned as T_LBRACE inside STEP_BODY + * (while bodies). 0 means no nested while bodies are open. Each T_LBRACE + * increments this; each T_RBRACE decrements it when depth > 0; when depth + * reaches 0 the '}' is the step's own closing brace → BEGIN(INITIAL). + */ +static int pgaf_step_brace_depth = 0; + +/* + * Nesting depth inside CLUSTER_BODY - so we know when the outermost "}" + * closes and we can return to INITIAL. + */ +static int pgaf_cluster_depth = 0; + +static char *pgaf_strdup(const char *s) +{ + char *r = strdup(s); + if (!r) { fprintf(stderr, "out of memory\n"); exit(1); } + return r; +} + +/* + * pgaf_read_raw_block: called when '{' is seen and we want to collect + * everything up to the matching '}' as a single string. Used for + * sql/expect content inside step bodies. + * Stores a freshly allocated trimmed string in yylval.str. + * + * Defined in the user-code section (after the second %%) so that it can + * call flex's static input() function. + */ +static void pgaf_read_raw_block(void); +%} + +%option noyywrap +%option nounput +%option never-interactive + +%x CLUSTER_BODY +%x STEP_BODY +%x EXEC_ARGS +%x EXEC_ARGS_REST + +%% + +"#"[^\n]* { /* comment */ } + +"cluster" { pgaf_next_brace_is_cluster = 1; return T_CLUSTER; } +"monitor" { return T_MONITOR; } +"node" { return T_NODE; } +"citus-coordinator" { return T_CITUS_COORDINATOR; } +"citus-worker" { return T_CITUS_WORKER; } + +"setup" { pgaf_next_brace_is_step = 1; return T_SETUP; } +"teardown" { pgaf_next_brace_is_step = 1; return T_TEARDOWN; } +"step" { pgaf_next_brace_is_step = 1; return T_STEP; } +"sequence" { return T_SEQUENCE; } + +"=" { return T_EQUALS; } + +[0-9]+[sS]? { + yylval.ival = atoi(yytext); + return T_INTEGER; +} + +[A-Za-z_][A-Za-z0-9_-]* { + yylval.str = pgaf_strdup(yytext); + return T_IDENT; +} + +\"[^\"]*\" { + yytext[yyleng - 1] = '\0'; + yylval.str = pgaf_strdup(yytext + 1); + return T_STRING; +} + +"{" { + if (pgaf_next_brace_is_step) + { + pgaf_next_brace_is_step = 0; + BEGIN(STEP_BODY); + return T_LBRACE; + } + if (pgaf_next_brace_is_cluster) + { + pgaf_next_brace_is_cluster = 0; + pgaf_cluster_depth = 1; + BEGIN(CLUSTER_BODY); + return T_LBRACE; + } + /* fallback: should not happen in a well-formed file */ + fprintf(stderr, "pgaftest: unexpected '{' at line %d\n", pgaf_line_number); +} + +\n { pgaf_line_number++; } +[ \t\r]+ { /* whitespace */ } + +. { + fprintf(stderr, "pgaftest: unexpected character '%c' at line %d\n", + yytext[0], pgaf_line_number); +} + +"#"[^\n]* { /* comment */ } +\n { pgaf_line_number++; } +[ \t\r]+ { /* whitespace */ } + +"monitor" { return T_MONITOR; } +"image-target" { return T_IMAGE_TARGET; } +"image" { return T_IMAGE; } +"ssl" { return T_SSL; } +"auth-method" { return T_AUTH_METHOD; } +"auth" { return T_AUTH; } +"formation" { return T_FORMATION; } +"num-sync" { return T_NUM_SYNC; } +"node" { return T_NODE; } + +"coordinator" { return T_COORDINATOR; } +"worker" { return T_WORKER; } +"async" { return T_ASYNC; } +"no-monitor" { return T_NO_MONITOR; } +"password" { return T_PASSWORD; } +"monitor-password" { return T_MONITOR_PASSWORD; } +"launch" { return T_LAUNCH; } +"deferred" { return T_DEFERRED; } +"immediate" { return T_IMMEDIATE; } +"initially" { return T_INITIALLY; } +"stopped" { return T_STOPPED; } +"volume" { return T_VOLUME; } +"listen" { return T_LISTEN; } +"citus-secondary" { return T_CITUS_SECONDARY; } +"candidate-priority" { return T_CANDIDATE_PRIORITY; } +"group" { return T_GROUP; } +"port" { return T_PORT; } +"citus-cluster-name" { return T_CITUS_CLUSTER_NAME; } +"debian-cluster" { return T_DEBIAN_CLUSTER; } +"replication-quorum" { return T_REPLICATION_QUORUM; } +"replication-password" { return T_REPLICATION_PASSWORD; } +"extension-version" { return T_EXTENSION_VERSION; } +"bind-source" { return T_BIND_SOURCE; } + +"=" { return T_EQUALS; } + +[0-9]+[sS]? { + yylval.ival = atoi(yytext); + return T_INTEGER; +} + +\"[^\"]*\" { + yytext[yyleng - 1] = '\0'; + yylval.str = pgaf_strdup(yytext + 1); + return T_STRING; +} + +"{" { + pgaf_cluster_depth++; + return T_LBRACE; +} + +"}" { + pgaf_cluster_depth--; + if (pgaf_cluster_depth == 0) + BEGIN(INITIAL); + return T_RBRACE; +} + +"init" { return T_FS_INIT; } +"single" { return T_FS_SINGLE; } +"primary" { return T_FS_PRIMARY; } +"wait_primary" { return T_FS_WAIT_PRIMARY; } +"wait-primary" { return T_FS_WAIT_PRIMARY; } +"wait_standby" { return T_FS_WAIT_STANDBY; } +"wait-standby" { return T_FS_WAIT_STANDBY; } +"demoted" { return T_FS_DEMOTED; } +"demote_timeout" { return T_FS_DEMOTE_TIMEOUT; } +"demote-timeout" { return T_FS_DEMOTE_TIMEOUT; } +"draining" { return T_FS_DRAINING; } +"secondary" { return T_FS_SECONDARY; } +"catchingup" { return T_FS_CATCHINGUP; } +"prepare_promotion" { return T_FS_PREP_PROMOTION; } +"prepare-promotion" { return T_FS_PREP_PROMOTION; } +"stop_replication" { return T_FS_STOP_REPLICATION; } +"stop-replication" { return T_FS_STOP_REPLICATION; } +"maintenance" { return T_FS_MAINTENANCE; } +"join_primary" { return T_FS_JOIN_PRIMARY; } +"join-primary" { return T_FS_JOIN_PRIMARY; } +"apply_settings" { return T_FS_APPLY_SETTINGS; } +"apply-settings" { return T_FS_APPLY_SETTINGS; } +"prepare_maintenance" { return T_FS_PREPARE_MAINTENANCE; } +"prepare-maintenance" { return T_FS_PREPARE_MAINTENANCE; } +"wait_maintenance" { return T_FS_WAIT_MAINTENANCE; } +"wait-maintenance" { return T_FS_WAIT_MAINTENANCE; } +"report_lsn" { return T_FS_REPORT_LSN; } +"report-lsn" { return T_FS_REPORT_LSN; } +"fast_forward" { return T_FS_FAST_FORWARD; } +"fast-forward" { return T_FS_FAST_FORWARD; } +"join_secondary" { return T_FS_JOIN_SECONDARY; } +"join-secondary" { return T_FS_JOIN_SECONDARY; } +"dropped" { return T_FS_DROPPED; } + +[A-Za-z_][A-Za-z0-9_-]* { + yylval.str = pgaf_strdup(yytext); + return T_IDENT; +} + +"#"[^\n]* { /* comment */ } +\n { pgaf_line_number++; } +[ \t\r]+ { /* whitespace */ } + +"exec-fails" { BEGIN(EXEC_ARGS); return T_EXEC_FAILS; } +"exec" { BEGIN(EXEC_ARGS); return T_EXEC; } +"pg_autoctl" { BEGIN(EXEC_ARGS); return T_PG_AUTOCTL; } + +"wait" { return T_WAIT; } +"until" { return T_UNTIL; } +"timeout" { return T_TIMEOUT; } +"assert" { return T_ASSERT; } +"sql" { return T_SQL; } +"expect" { return T_EXPECT; } +"error" { return T_ERROR; } +"promote" { return T_PROMOTE; } +"network" { return T_NETWORK; } +"disconnect" { return T_DISCONNECT; } +"connect" { return T_CONNECT; } +"sleep" { return T_SLEEP; } +"compose" { return T_COMPOSE; } +"down" { return T_DOWN; } +"start" { return T_START; } +"stop" { return T_STOP; } +"stopped" { return T_STOPPED; } +"kill" { return T_KILL; } +"in" { return T_IN; } +"state" { return T_STATE; } +"assigned-state" { return T_ASSIGNED_STATE; } +"candidate-priority" { return T_CANDIDATE_PRIORITY; } +"group" { return T_GROUP; } +"and" { return T_AND; } +"is" { return T_IS; } +"with" { return T_WITH; } +"=" { return T_EQUALS; } +"," { return T_COMMA; } +"postgres" { return T_POSTGRES; } +"stays" { return T_STAYS; } +"while" { return T_WHILE; } +"through" { return T_THROUGH; } +"set" { return T_SET; } +"inject" { BEGIN(EXEC_ARGS); return T_INJECT; } +"logs" { return T_LOGS; } +"not" { return T_NOT; } +"contains" { return T_CONTAINS; } +"matches" { return T_MATCHES; } + +[0-9]+[sS]? { + yylval.ival = atoi(yytext); + return T_INTEGER; +} + +\"[^\"]*\" { + yytext[yyleng - 1] = '\0'; + yylval.str = pgaf_strdup(yytext + 1); + return T_STRING; +} + +"{" { + if (pgaf_next_brace_is_while) { + pgaf_next_brace_is_while = 0; + pgaf_step_brace_depth++; + return T_LBRACE; + } + pgaf_read_raw_block(); + return T_BLOCK; +} + +"}" { + if (pgaf_step_brace_depth > 0) { + pgaf_step_brace_depth--; + return T_RBRACE; + } + pgaf_step_brace_depth = 0; + BEGIN(INITIAL); + return T_RBRACE; +} + +[A-Za-z_][A-Za-z0-9_-]* { + yylval.str = pgaf_strdup(yytext); + return T_IDENT; +} + +[ \t]+ { /* skip whitespace before service name */ } + +[^ \t\n]+ { + yylval.str = pgaf_strdup(yytext); + BEGIN(EXEC_ARGS_REST); + return T_IDENT; +} + +\n { + pgaf_line_number++; + BEGIN(STEP_BODY); +} + +[^\n]+ { + char *p = yytext; + while (*p == ' ' || *p == '\t') p++; + yylval.str = pgaf_strdup(p); + BEGIN(STEP_BODY); + return T_SHELL_ARGS; +} + +\n { + pgaf_line_number++; + BEGIN(STEP_BODY); +} + +%% + +static void +pgaf_read_raw_block(void) +{ + int depth = 1; + int c; + char buf[65536]; + int pos = 0; + + while (depth > 0 && (c = input()) != EOF) + { + if (c == '{') depth++; + else if (c == '}') { depth--; if (depth == 0) break; } + else if (c == '\n') pgaf_line_number++; + if (depth > 0 && pos < (int)sizeof(buf) - 1) + buf[pos++] = (char) c; + } + buf[pos] = '\0'; + + char *start = buf; + while (*start == ' ' || *start == '\t' || *start == '\n' || *start == '\r') + start++; + char *end = start + strlen(start); + while (end > start && + (end[-1] == ' ' || end[-1] == '\t' || + end[-1] == '\n' || end[-1] == '\r')) + end--; + *end = '\0'; + + yylval.str = pgaf_strdup(start); +} diff --git a/tests/tap/README.md b/tests/tap/README.md new file mode 100644 index 000000000..47c6ab798 --- /dev/null +++ b/tests/tap/README.md @@ -0,0 +1,217 @@ +# pg_auto_failover TAP Test Suite + +Tests are written in the `.pgaf` spec DSL and run by the `pgaftest` binary. +Output is [TAP](https://testanything.org/) (Test Anything Protocol), +compatible with `prove` and any TAP harness. + +## Prerequisites + +- `pgaftest` built: `make -C src/bin` +- Docker with the Compose plugin (`docker compose version`) + +## Running tests + +### All tests (via schedule) + +```sh +pgaftest run --schedule tests/tap/schedule +``` + +Runs every spec listed in `tests/tap/schedule` and emits TAP on stdout. +Exit code is 0 only when all tests pass. + +With `prove` for a formatted summary: + +```sh +pgaftest run --schedule tests/tap/schedule | prove --tap - +``` + +### A single spec + +```sh +pgaftest run tests/tap/specs/basic_operation.pgaf +``` + +### Interactive mode (live cluster, shell access) + +Brings up the cluster, runs `setup {}`, then drops you into a shell: + +```sh +pgaftest setup tests/tap/specs/basic_operation.pgaf +``` + +Run individual steps against the live cluster: + +```sh +pgaftest step stop_primary tests/tap/specs/basic_operation.pgaf +pgaftest step check_failover tests/tap/specs/basic_operation.pgaf +``` + +Inspect the generated `docker-compose.yml` without starting anything: + +```sh +pgaftest show tests/tap/specs/basic_operation.pgaf +``` + +Tear down when done: + +```sh +pgaftest down tests/tap/specs/basic_operation.pgaf +``` + +## Spec file format + +Each `.pgaf` file in `specs/` is a complete, self-contained test scenario. + +``` +cluster { + monitor + formation { + node1 + node2 candidate-priority 0 + } +} + +setup { + wait until node1 state is primary timeout 120s + wait until node2 state is secondary timeout 120s +} + +teardown { + compose down +} + +step stop_primary { + compose kill node1 +} + +step check_failover { + wait until node2 state is primary timeout 90s + assert node2 state is primary +} + +sequence + stop_primary + check_failover +``` + +| Block | Purpose | +|---|---| +| `cluster {}` | Topology — drives `docker-compose.yml` generation | +| `setup {}` | Run after `compose up -d`, before the first step | +| `teardown {}` | Always run at end (CI); on-demand in interactive mode | +| `step name {}` | Named command block | +| `sequence` | Step execution order for `pgaftest run` | + +### Commands inside blocks + +**Process control** + +| Command | Effect | +|---|---| +| `exec ` | `docker compose exec -T ` — fails if exit ≠ 0 | +| `exec-fails ` | Same but asserts non-zero exit | +| `compose down` | `docker compose down --volumes` | +| `compose start ` | `docker compose start ` | +| `compose stop ` | `docker compose stop ` | +| `compose kill ` | `docker compose kill ` (SIGKILL) | +| `stop postgres ` | Stop Postgres inside the container without stopping the keeper | +| `start postgres ` | Restart Postgres inside the container via the keeper | + +**State and timing** + +| Command | Effect | +|---|---| +| `wait until state is [timeout Ns]` | Poll monitor; fail on timeout | +| `wait until assigned-state is [timeout Ns]` | Poll assigned state | +| `wait until stopped [timeout Ns]` | Wait until container exits | +| `wait until , , … [in group N] [timeout Ns]` | Formation-wide state convergence | +| `wait until state is and state is … [timeout Ns]` | Multi-node simultaneous wait | +| `assert state is ` | Instant check; no polling | +| `assert assigned-state is ` | Check assigned state column | +| `assert stays while { … }` | Assert state unchanged throughout body | +| `sleep Ns` | Wait N seconds | + +**SQL and expectations** + +| Command | Effect | +|---|---| +| `sql { SQL }` | Run SQL on service, capture output | +| `expect { text }` | Substring-match last `sql` output | +| `expect { { row } { row } }` | Tuple form: match `psql --tuples-only --no-align` rows | +| `expect error [SQLSTATE]` | Assert previous `sql` raised a SQL error | + +**Network** + +| Command | Effect | +|---|---| +| `network disconnect ` | `docker network disconnect` — simulate partition | +| `network connect ` | `docker network connect` — restore | + +**Log inspection** + +| Command | Effect | +|---|---| +| `logs contains ""` | Assert literal `` appears in container logs | +| `logs not contains ""` | Assert literal `` does NOT appear | +| `logs matches ""` | Assert PCRE pattern matches a log line (`grep -P`) | +| `logs not matches ""` | Assert PCRE pattern does NOT match any log line | + +**Cluster lifecycle** + +| Command | Effect | +|---|---| +| `promote [, , …]` | `pg_autoctl perform promotion` per node | +| `set monitor ` | Switch runner's active monitor (for replace-monitor tests) | + +### `%CIDR%` macro + +The literal `%CIDR%` is expanded to the Docker network CIDR in any `exec` +or `exec-fails` argument. Use it to scope HBA rules to the test network: + +``` +exec monitor bash -c "echo 'host all all %CIDR% trust' >> $PGDATA/pg_hba.conf" +``` + +## Schedule file + +`tests/tap/schedule` lists one spec *name* per line (no path, no `.pgaf` +extension). Lines starting with `#` are comments. + +``` +basic_operation +multi_standbys +# citus_basic # requires Citus image — set CITUS_CLUSTER=1 +``` + +## Relationship to Python tests + +Each `.pgaf` spec was ported from a Python integration test in `tests/`. +The original is recorded in the spec header: + +``` +# Predecessor: tests/test_basic_operation.py +``` + +The pgaf framework replaces the Python `pgautofailover_utils` subprocess +harness with declarative DSL commands backed by `docker compose`. Node +creation, cluster initialization, and teardown are handled by the cluster +declaration and `compose up / down`; test steps focus purely on the +scenario logic. + +## CI integration + +```yaml +- name: Run pg_auto_failover tests + run: | + make -C src/bin pgaftest + pgaftest run --schedule tests/tap/schedule +``` + +Or with `prove`: + +```yaml +- run: pgaftest run --schedule tests/tap/schedule | prove --tap - +``` + +For the full DSL reference see [`docs/pgaftest.rst`](../../docs/pgaftest.rst). diff --git a/tests/tap/schedule b/tests/tap/schedule new file mode 100644 index 000000000..a58a8f966 --- /dev/null +++ b/tests/tap/schedule @@ -0,0 +1,36 @@ +# tests/tap/schedule +# One spec name per line; pgaftest run --schedule tests/tap/schedule +# Lines starting with # are comments; empty lines are ignored. +# Citus specs require CITUS_CLUSTER=1 env var to be set. + +basic_operation +basic_operation_listen_flag +maintenance_and_drop +create_standby_with_pgdata +ensure +monitor_disabled +replace_monitor +config_get_set +skip_pg_hba +#debian_clusters +auth +enable_ssl +ssl_cert +ssl_self_signed +multi_standbys +multi_async +multi_ifdown +multi_maintenance +multi_alternate +extension_update +installcheck +# Upgrade test — requires pgaf:current and pgaf:next images to be pre-built: +# make -C tests/upgrade pgaf-current pgaf-next +upgrade +# Citus tests (uncomment when CITUS_CLUSTER=1): +# basic_citus_operation +# citus_cluster_name +# citus_force_failover +# citus_multi_standbys +# citus_skip_pg_hba +# nonha_citus_operation diff --git a/tests/tap/specs/auth.pgaf b/tests/tap/specs/auth.pgaf new file mode 100644 index 000000000..4301c2492 --- /dev/null +++ b/tests/tap/specs/auth.pgaf @@ -0,0 +1,76 @@ +# Test md5 authentication between nodes and the monitor, including password +# handling and verifying that passwords are not leaked in logs. +# +# Ported from tests/test_auth.py. +# +# Password handling +# ----------------- +# All passwords are declared in the cluster block and written into the ini +# files by compose_gen before any container starts: +# +# cluster.monitorPassword → monitor.ini [pg_auto_failover] autoctl_node_password +# → every node.ini [monitor] pguri (credential in URI) +# → pgaftest service env PG_AUTOCTL_MONITOR (pguri) +# node.monitorPassword → node.ini [pg_auto_failover] monitor_password +# used by fsm_init_primary() when creating the +# pgautofailover_monitor health-check role +# node.replicationPassword → node.ini [replication] password +# used when creating pgautofailover_replicator +# +# All passwords are therefore in place before pg_autoctl runs its first FSM +# transition, removing any need for sequential startup or manual exec steps. +# Predecessor: tests/test_auth.py + +cluster { + monitor password "pg-auto-failover" + auth md5 + formation auth { + node1 monitor-password "pg-hba-check" replication-password "pg-replication" + node2 monitor-password "pg-hba-check" replication-password "pg-replication" + } +} + +setup { + wait until primary, secondary timeout 120s + promote node1 +} + +teardown { + compose down +} + +step test_001_init_primary { + sql node1 { SHOW synchronous_standby_names; } + expect { ANY 1 (pgautofailover_standby_ } +} + +step test_002_create_t1 { + sql node1 { CREATE TABLE t1(a int); } + sql node1 { INSERT INTO t1 VALUES (1), (2); } + sql node2 { SELECT count(*) FROM t1; } + expect { 2 } +} + +step test_003_failover { + exec monitor pg_autoctl perform failover --formation auth + wait until node2 state is primary + and node1 state is secondary + timeout 90s + wait until node2 state is primary timeout 90s + wait until node1 state is secondary timeout 90s + sql node2 { SHOW synchronous_standby_names; } + expect { ANY 1 (pgautofailover_standby_ } +} + +step test_004_verify_data { + sql node2 { INSERT INTO t1 VALUES (3); } + sql node1 { SELECT count(*) FROM t1; } + expect { 3 } +} + +step test_005_logging_of_passwords { + logs node2 not contains "monitor_password" + logs node2 not contains "streaming_password" + logs node2 contains "monitor-password ****" + logs node2 contains "replication-password ****" +} diff --git a/tests/tap/specs/basic_citus_operation.pgaf b/tests/tap/specs/basic_citus_operation.pgaf new file mode 100644 index 000000000..809c1d6cf --- /dev/null +++ b/tests/tap/specs/basic_citus_operation.pgaf @@ -0,0 +1,132 @@ +# Test basic Citus cluster operations: coordinator HA, worker HA with two +# worker groups, distributed table writes/reads, and failover at each level. +# +# Ported from tests/test_basic_citus_operation.py +# Predecessor: tests/test_basic_citus_operation.py + +cluster { + monitor + formation { + coordinator1a coordinator + coordinator1b coordinator + worker1a worker group 1 + worker1b worker group 1 + worker2a worker group 2 + worker2b worker group 2 + } +} + +setup { + wait until primary, secondary in group 0 timeout 90s + wait until primary, secondary in group 1 timeout 90s + wait until primary, secondary in group 2 timeout 90s + promote coordinator1a, worker1a, worker2a +} + +teardown { + compose down +} + +# +# test_001: coordinator pair comes up +# + +step test_001_init_coordinator { + wait until coordinator1a state is primary + and coordinator1b state is secondary + timeout 90s +} + +# +# test_002: worker groups come up +# + +step test_002_init_workers { + wait until worker1a state is primary + and worker1b state is secondary + and worker2a state is primary + and worker2b state is secondary + timeout 90s +} + +step test_002b_wait_metadata_sync { + exec coordinator1a sh -c 'for i in $(seq 1 30); do n=$(psql -U docker -d demo -tAc "SELECT count(*) FROM pg_dist_node WHERE metadatasynced = false AND isactive = true"); [ "$n" = "0" ] && exit 0; sleep 2; done; exit 1' + sql coordinator1a { + CREATE OR REPLACE FUNCTION public.wait_until_metadata_sync(timeout + INTEGER DEFAULT 15000) RETURNS void LANGUAGE C STRICT AS 'citus'; + } + sql coordinator1a { SELECT public.wait_until_metadata_sync(); } +} + +# +# test_003: create distributed table +# + +step test_003_create_distributed_table { + sql coordinator1a { CREATE TABLE t1 (a int); } + sql coordinator1a { SELECT create_distributed_table('t1', 'a'); } + sql coordinator1a { INSERT INTO t1 VALUES (1), (2); } +} + +step test_004_001_fail_worker2 { + network disconnect worker2a + wait until worker2b state is wait_primary timeout 90s +} + +step test_004_003_insert_while_wait_primary { + wait until worker2b state is wait_primary timeout 90s + sql coordinator1a { INSERT INTO t1 VALUES (3); } +} + +step test_004_004_reconnect_worker2a { + network connect worker2a + wait until worker2b state is primary + and worker2a state is secondary + timeout 180s +} + +step test_005_read_from_workers_via_coordinator { + sql coordinator1a { SELECT a FROM t1 ORDER BY a ASC; } + expect { { 1 } { 2 } { 3 } } +} + +# +# test_006: write more rows +# + +step test_006_writes_to_coordinator_succeed { + sql coordinator1a { INSERT INTO t1 VALUES (4); } + sql coordinator1a { SELECT a FROM t1 ORDER BY a ASC; } + expect { { 1 } { 2 } { 3 } { 4 } } +} + +step test_007_fail_worker2b { + network disconnect worker2b + wait until worker2a state is wait_primary timeout 90s +} + +step test_007b_reconnect_worker2b { + network connect worker2b + wait until worker2a state is primary + and worker2b state is secondary + timeout 180s +} + +step test_008_read_from_workers_via_coordinator { + sql coordinator1a { SELECT a FROM t1 ORDER BY a ASC; } + expect { { 1 } { 2 } { 3 } { 4 } } +} + +step test_009_perform_failover_worker2 { + exec monitor pg_autoctl perform failover --group 2 + wait until worker2b state is primary + and worker2a state is secondary + timeout 180s +} + +step test_010_perform_failover_coordinator { + exec monitor pg_autoctl perform failover --group 0 + wait until coordinator1a state is secondary + and coordinator1b state is primary + timeout 90s +} diff --git a/tests/tap/specs/basic_operation.pgaf b/tests/tap/specs/basic_operation.pgaf new file mode 100644 index 000000000..7dfae59db --- /dev/null +++ b/tests/tap/specs/basic_operation.pgaf @@ -0,0 +1,402 @@ +# Basic two-node HA: create primary + secondary, test maintenance, +# failover, network partition detection, and node drop. +# +# Ported from tests/test_basic_operation.py — same step ordering and +# same commands as the Python implementation (run-on-node vs run-on-monitor +# matches the Python test). +# +# Node IDs are deterministic because node2's container depends_on node1 +# (see compose_gen.c), so node1 always registers first and gets nodeid=1, +# node2 gets nodeid=2, node3 gets nodeid=3. Replication slot names embed +# the standby's nodeid: +# pgautofailover_standby_1 — node1's slot (on the primary) +# pgautofailover_standby_2 — node2's slot (on the primary) +# pgautofailover_standby_3 — node3's slot (on the primary) +# +# node3 is declared launch deferred so the container starts with +# `sleep infinity`; test_017 issues `compose start node3` which +# restarts it with the normal `pg_autoctl node run` command. +# Predecessor: tests/test_basic_operation.py + +cluster { + monitor + formation { + node1 + node2 + node3 launch deferred + } +} + +setup { + wait until primary, secondary timeout 120s + promote node1 +} + +teardown { + compose down +} + +# +# test_003: create table on primary and verify replication to secondary. +# + +step test_003_create_t1 { + sql node1 { CREATE TABLE t1(a int); INSERT INTO t1 VALUES (1), (2); } + sql node2 { SELECT count(*) FROM t1; } + expect { 2 } +} + +# +# test_004: verify synchronous_standby_names and replication slots +# after secondary joins. Both nodes must have a physical slot +# for the other's replication identity. +# + +step test_004_init_secondary { + sql node1 { SHOW synchronous_standby_names; } + expect { ANY 1 (pgautofailover_standby_2) } + sql node1 { + SELECT slot_name + FROM pg_replication_slots + WHERE slot_name ~ '^pgautofailover_standby_' AND slot_type = 'physical' + ORDER BY slot_name; + } + expect { pgautofailover_standby_2 } + sql node2 { + SELECT slot_name + FROM pg_replication_slots + WHERE slot_name ~ '^pgautofailover_standby_' AND slot_type = 'physical' + ORDER BY slot_name; + } + expect { pgautofailover_standby_1 } +} + +# +# test_005: read replicated data from the secondary. +# + +step test_005_read_from_secondary { + sql node2 { SELECT * FROM t1 ORDER BY a; } + expect { { 1 } { 2 } } +} + +# +# test_006: verify the secondary rejects writes (read-only replica). +# INSERT must fail with SQLSTATE 25006 (read_only_sql_transaction). +# + +step test_006_001_writes_to_node2_fail { + sql node2 { INSERT INTO t1 VALUES (3); } + expect error 25006 +} + +step test_006_002_read_from_secondary { + sql node2 { SELECT * FROM t1 ORDER BY a; } + expect { { 1 } { 2 } } +} + +# +# test_007: maintenance mode on node1 (primary). +# test_007_002: enabling maintenance without --allow-failover on the primary +# must fail (no standby would remain to take over). +# test_007_004: with --allow-failover node2 transitions to wait_primary. +# test_007_005: disabling maintenance rejoins node1 as secondary. +# + +step test_007_002_maintenance_primary { + exec-fails node1 pg_autoctl enable maintenance +} + +step test_007_004_maintenance_primary_allow_failover { + exec node1 pg_autoctl enable maintenance --allow-failover + wait until node1 state is maintenance + and node2 state is wait_primary + timeout 60s + sql node2 { SHOW synchronous_standby_names; } + expect { } +} + +step test_007_005_disable_maintenance { + exec node1 pg_autoctl disable maintenance + wait until node1 state is secondary + and node2 state is primary + timeout 90s + sql node2 { SHOW synchronous_standby_names; } + expect { ANY 1 (pgautofailover_standby_1) } +} + +# +# test_008: maintenance mode on node1 (now secondary after test_007). +# test_008_001: enable maintenance, stop postgres on node1, write to primary. +# test_008_002: disable maintenance — node1 pg_rewinds and rejoins as secondary. +# + +step test_008_001_enable_maintenance_secondary { + exec node1 pg_autoctl enable maintenance + wait until node1 state is maintenance timeout 60s + stop postgres node1 + sql node2 { INSERT INTO t1 VALUES (3); } +} + +step test_008_002_disable_maintenance_secondary { + exec node1 pg_autoctl disable maintenance + wait until node1 state is secondary + and node2 state is primary + timeout 90s + sql node2 { SHOW synchronous_standby_names; } + expect { ANY 1 (pgautofailover_standby_1) } +} + +# +# test_009: failback — promote node1 back to primary via perform-promotion. +# After: node1=primary, node2=secondary. +# + +step test_009_failback { + exec monitor pg_autoctl perform promotion --name node1 + wait until node2 state is secondary + and node1 state is primary + timeout 90s + sql node1 { SHOW synchronous_standby_names; } + expect { ANY 1 (pgautofailover_standby_2) } +} + +# +# test_010: disconnect node1 (primary) from the network — simulates a network +# partition. node2 must detect the loss and enter wait_primary +# (it cannot promote yet without confirmation from the monitor). +# + +step test_010_fail_primary { + network disconnect node1 + wait until node2 state is wait_primary timeout 120s +} + +# +# test_011: node2 is now wait_primary — writes must succeed even before +# the full promotion completes (no sync standby required in +# wait_primary state). +# + +step test_011_writes_to_node2_succeed { + sql node2 { INSERT INTO t1 VALUES (4); } + sql node2 { SELECT * FROM t1 ORDER BY a; } + expect { { 1 } { 2 } { 3 } { 4 } } +} + +# +# test_012: reconnect node1 — it pg_rewinds against node2 and rejoins as +# secondary. node2 transitions from wait_primary to primary once +# it has a healthy sync standby again. +# + +step test_012_start_node1_again { + network connect node1 + wait until node2 state is primary + and node1 state is secondary + timeout 90s + sql node2 { SHOW synchronous_standby_names; } + expect { ANY 1 (pgautofailover_standby_1) } +} + +# +# test_013: verify all four rows are visible on node1 (now secondary), +# confirming pg_rewind brought it fully up to date. +# + +step test_013_read_from_new_secondary { + sql node1 { SELECT * FROM t1 ORDER BY a; } + expect { { 1 } { 2 } { 3 } { 4 } } +} + +# +# test_014: node1 is secondary again — writes must be rejected (25006). +# + +step test_014_writes_to_node1_fail { + sql node1 { INSERT INTO t1 VALUES (5); } + expect error 25006 +} + +# +# test_015: stop node1 (secondary) via compose stop — Postgres shuts down +# cleanly. node2 enters wait_primary because its sync standby +# disappeared (number_sync_standbys=1). +# + +step test_015_fail_secondary { + compose stop node1 + wait until node1 stopped timeout 30s + wait until node2 state is wait_primary timeout 60s +} + +# +# test_016: restart node1, let it rejoin, then drop it with --no-wait. +# node2 becomes single. Verify no stale replication slots remain +# on node2 after the drop (pg_auto_failover must clean them up). +# + +step test_016_drop_secondary { + compose start node1 + wait until node2 state is primary + and node1 state is secondary + timeout 90s + exec node1 pg_autoctl drop node --no-wait + wait until node1 stopped timeout 30s + wait until node2 state is single timeout 90s + sql node2 { + SELECT slot_name + FROM pg_replication_slots + WHERE slot_name ~ '^pgautofailover_standby_' AND slot_type = 'physical' + ORDER BY slot_name; + } + expect { } +} + +# +# test_017–019: add node3 as a new secondary (was launch deferred). +# test_017: issue `pg_autoctl node start` to bring the deferred node live. +# test_018: perform failover must fail immediately — node3 is still +# catching up and no election can complete yet. +# test_019: wait for node3 to reach secondary; verify SSN and slots. +# + +step test_017_add_new_secondary { + exec node3 pg_autoctl node start +} + +step test_018_cant_failover_yet { + exec-fails monitor pg_autoctl perform failover +} + +step test_019_run_secondary { + wait until node3 state is secondary + and node2 state is primary + timeout 120s + sql node2 { SHOW synchronous_standby_names; } + expect { ANY 1 (pgautofailover_standby_3) } + sql node2 { + SELECT slot_name + FROM pg_replication_slots + WHERE slot_name ~ '^pgautofailover_standby_' AND slot_type = 'physical' + ORDER BY slot_name; + } + expect { pgautofailover_standby_3 } + sql node3 { + SELECT slot_name + FROM pg_replication_slots + WHERE slot_name ~ '^pgautofailover_standby_' AND slot_type = 'physical' + ORDER BY slot_name; + } + expect { pgautofailover_standby_2 } +} + +# +# test_020: two back-to-back manual failovers. After each failover verify +# synchronous_standby_names and that every node has the correct +# physical replication slot for the other nodes — slots must +# survive a role change intact. +# + +step test_020_multiple_manual_failover_verify_replication_slots { + exec monitor pg_autoctl perform failover + wait until node3 state is primary + and node2 state is secondary + timeout 90s + sql node3 { SHOW synchronous_standby_names; } + expect { ANY 1 (pgautofailover_standby_2) } + sql node3 { + SELECT slot_name + FROM pg_replication_slots + WHERE slot_name ~ '^pgautofailover_standby_' AND slot_type = 'physical' + ORDER BY slot_name; + } + expect { pgautofailover_standby_2 } + sql node2 { + SELECT slot_name + FROM pg_replication_slots + WHERE slot_name ~ '^pgautofailover_standby_' AND slot_type = 'physical' + ORDER BY slot_name; + } + expect { pgautofailover_standby_3 } + exec node2 pg_autoctl perform promotion + wait until node2 state is primary + and node3 state is secondary + timeout 90s + sql node2 { SHOW synchronous_standby_names; } + expect { ANY 1 (pgautofailover_standby_3) } + sql node2 { + SELECT slot_name + FROM pg_replication_slots + WHERE slot_name ~ '^pgautofailover_standby_' AND slot_type = 'physical' + ORDER BY slot_name; + } + expect { pgautofailover_standby_3 } + sql node3 { + SELECT slot_name + FROM pg_replication_slots + WHERE slot_name ~ '^pgautofailover_standby_' AND slot_type = 'physical' + ORDER BY slot_name; + } + expect { pgautofailover_standby_2 } +} + +# +# test_021–023: network partition detection and demote-timeout. +# test_021: disconnect node2 (primary) — simulates a hard network failure. +# test_022: wait for demote_timeout on node2 and wait_primary on node3; +# verify pg_autoctl on node2 considers itself unready (no monitor +# contact) and that node3 has no sync standby yet. +# test_023: reconnect node2 — it detects it lost the election, pg_rewinds +# against node3, and rejoins as secondary. +# + +step test_021_ifdown_primary { + wait until node2 state is primary timeout 30s + sql node2 { SHOW synchronous_standby_names; } + expect { ANY 1 (pgautofailover_standby_3) } + network disconnect node2 +} + +step test_022_detect_network_partition { + wait until node2 state is demote_timeout timeout 90s + wait until node3 state is wait_primary timeout 90s + sleep 3s + exec-fails node2 pg_autoctl inspect pgsetup ready + sql node3 { SHOW synchronous_standby_names; } + expect { } +} + +step test_023_ifup_old_primary { + network connect node2 + wait until node2 state is secondary + and node3 state is primary + timeout 90s + sql node3 { SHOW synchronous_standby_names; } + expect { ANY 1 (pgautofailover_standby_2) } +} + +# +# test_024: stop the monitor's Postgres process directly (bypassing +# pg_autoctl) and verify pg_autoctl restarts it automatically. +# The data node must remain in its current state throughout — +# a brief monitor outage must not trigger a spurious failover. +# + +step test_024_stop_postgres_monitor { + assert node3 stays primary while { + stop postgres monitor + sleep 5s + } + wait until node3 state is primary timeout 60s +} + +# +# test_025: drop the last remaining data node — cluster goes empty. +# + +step test_025_drop_primary { + exec node3 pg_autoctl drop node --no-wait + wait until node3 stopped timeout 60s + wait until node2 state is single timeout 90s +} diff --git a/tests/tap/specs/basic_operation_listen_flag.pgaf b/tests/tap/specs/basic_operation_listen_flag.pgaf new file mode 100644 index 000000000..830f05c3d --- /dev/null +++ b/tests/tap/specs/basic_operation_listen_flag.pgaf @@ -0,0 +1,117 @@ +# Test basic pg_auto_failover operations with the --listen flag, verifying +# that nodes bind on all interfaces for replication and failover. +# +# Ported from tests/test_basic_operation_listen_flag.py +# Predecessor: tests/test_basic_operation_listen_flag.py + +cluster { + monitor + formation { + node1 listen + node2 launch deferred listen + } +} + +setup { + exec monitor pg_autoctl inspect pgsetup wait +} + +teardown { + compose down +} + +# +# test_001: init primary with listen flag +# + +step test_001_init_primary { + wait until node1 state is single timeout 60s + exec node1 pg_autoctl inspect pgsetup wait +} + +# +# test_002: create table t1 +# + +step test_002_create_t1 { + sql node1 { CREATE TABLE t1(a int); } + sql node1 { INSERT INTO t1 VALUES (1), (2); } +} + +# +# test_003: add secondary node2 with listen flag +# + +step test_003_init_secondary { + exec node2 pg_autoctl node start + wait until node2 state is secondary + and node1 state is primary + timeout 90s +} + +# +# test_004: read from secondary +# + +step test_004_read_from_secondary { + sql node2 { SELECT * FROM t1 ORDER BY a; } + expect { { 1 } { 2 } } +} + +step test_005_writes_to_node2_fail { + sql node2 { INSERT INTO t1 VALUES (3); } + expect error 25006 +} + +# +# test_006: fail primary node1 +# + +step test_006_fail_primary { + network disconnect node1 + wait until node2 state is wait_primary timeout 90s +} + +# +# test_007: writes to new primary succeed +# + +step test_007_writes_to_node2_succeed { + sql node2 { INSERT INTO t1 VALUES (3); } + sql node2 { SELECT * FROM t1 ORDER BY a; } + expect { { 1 } { 2 } { 3 } } +} + +# +# test_008: restart node1 as secondary +# + +step test_008_start_node1_again { + network connect node1 + wait until node2 state is primary + and node1 state is secondary + timeout 90s +} + +# +# test_009: read from new secondary (node1) +# + +step test_009_read_from_new_secondary { + sql node1 { SELECT * FROM t1 ORDER BY a; } + expect { { 1 } { 2 } { 3 } } +} + +step test_010_writes_to_node1_fail { + sql node1 { INSERT INTO t1 VALUES (3); } + expect error 25006 +} + +# +# test_011: fail secondary node1 +# + +step test_011_fail_secondary { + network disconnect node1 + wait until node2 state is wait_primary timeout 90s +} diff --git a/tests/tap/specs/citus_cluster_name.pgaf b/tests/tap/specs/citus_cluster_name.pgaf new file mode 100644 index 000000000..ef84bee4d --- /dev/null +++ b/tests/tap/specs/citus_cluster_name.pgaf @@ -0,0 +1,120 @@ +# Test Citus cluster name and read-only secondary cluster routing. +# +# A "readonly" cluster is a set of Citus nodes registered with +# candidate-priority=0, citus-secondary, and citus-cluster-name=readonly. +# These nodes should have: +# citus.use_secondary_nodes = 'always' +# citus.cluster_name = 'readonly' +# in their postgresql.conf, so that distributed queries are routed to the +# secondary (standby) copies of each shard rather than the primaries. +# +# Ported from tests/test_citus_cluster_name.py. +# Predecessor: tests/test_citus_cluster_name.py + +cluster { + monitor + formation { + coordinator1a coordinator + node coordinator1b { + coordinator + candidate-priority 0 + citus-secondary + citus-cluster-name readonly + } + worker1a worker group 1 + node worker1b { + worker group 1 + candidate-priority 0 + citus-secondary + citus-cluster-name readonly + } + worker2a worker group 2 + node worker2b { + worker group 2 + candidate-priority 0 + citus-secondary + citus-cluster-name readonly + } + } +} + +setup { + wait until primary, secondary in group 0 timeout 90s + wait until primary, secondary in group 1 timeout 90s + wait until primary, secondary in group 2 timeout 90s + promote coordinator1a, worker1a, worker2a +} + +teardown { + compose down +} + +# +# test_001: coordinator pair — coordinator1b is read-only secondary cluster +# + +step test_001_init_coordinator { + wait until coordinator1a state is primary + and coordinator1b state is secondary + timeout 90s +} + +step test_002_001_init_workers { + wait until worker1a state is primary + and worker1b state is secondary + timeout 90s +} + +step test_002_002_init_workers { + wait until worker2a state is primary + and worker2b state is secondary + timeout 90s +} + +# +# test_003: create distributed table and insert data +# + +step test_003_create_distributed_table { + sql coordinator1a { + SET citus.shard_count TO 4; CREATE TABLE t1 (a int); SELECT + create_distributed_table('t1', 'a'); + } + sql coordinator1a { + INSERT INTO t1 SELECT x FROM generate_series(1, 1000) as gs(x); + } + sql coordinator1a { CHECKPOINT; } +} + +# +# test_004: verify read-only cluster settings +# + +step test_004_read_only_cluster { + sql coordinator1a { + SELECT current_setting('citus.use_secondary_nodes'), + current_setting('citus.cluster_name'); + } + expect { never|default } + sql coordinator1b { + SELECT current_setting('citus.use_secondary_nodes'), + current_setting('citus.cluster_name'); + } + expect { always|readonly } + sql coordinator1b { SELECT count(*) FROM t1; } + expect { 1000 } +} + +# +# test_005: drop distributed table (cleanup) +# + +step test_005_drop_table { + exec coordinator1a sh -c 'for i in $(seq 1 30); do n=$(psql -U docker -d demo -tAc "SELECT count(*) FROM pg_dist_node WHERE metadatasynced = false AND isactive = true AND nodecluster = '"'"'default'"'"'"); [ "$n" = "0" ] && exit 0; sleep 2; done; exit 1' + sql coordinator1a { + CREATE OR REPLACE FUNCTION public.wait_until_metadata_sync(timeout + INTEGER DEFAULT 15000) RETURNS void LANGUAGE C STRICT AS 'citus'; + } + sql coordinator1a { SELECT public.wait_until_metadata_sync(); } + sql coordinator1a { DROP TABLE t1; } +} diff --git a/tests/tap/specs/citus_force_failover.pgaf b/tests/tap/specs/citus_force_failover.pgaf new file mode 100644 index 000000000..858b88787 --- /dev/null +++ b/tests/tap/specs/citus_force_failover.pgaf @@ -0,0 +1,86 @@ +# Test Citus force failover: a transaction in progress competes with a worker +# failover. The failover mechanism must terminate competing backends to make +# progress within a bounded time window. +# Also tests dropping a failed primary worker and a failed coordinator. +# +# Predecessor: tests/test_citus_force_failover.py + +cluster { + monitor + formation { + coordinator1a coordinator + coordinator1b coordinator + worker1a worker group 1 + worker1b worker group 1 + worker2a worker group 2 + worker2b worker group 2 + } +} + +setup { + wait until primary, secondary in group 0 timeout 90s + wait until primary, secondary in group 1 timeout 90s + wait until primary, secondary in group 2 timeout 90s + promote coordinator1a, worker1a, worker2a +} + +teardown { + compose down +} + +# +# test_001: coordinator pair +# + +step test_001_init_coordinator { + wait until coordinator1a state is primary + and coordinator1b state is secondary + timeout 90s +} + +# +# test_002: worker groups +# + +step test_002_init_workers { + wait until worker1a state is primary + and worker1b state is secondary + and worker2a state is primary + and worker2b state is secondary + timeout 90s +} + +# +# test_003: create distributed table +# + +step test_003_create_distributed_table { + exec coordinator1a sh -c 'for i in $(seq 1 30); do n=$(psql -U docker -d demo -tAc "SELECT count(*) FROM pg_dist_node WHERE metadatasynced = false AND isactive = true"); [ "$n" = "0" ] && exit 0; sleep 2; done; exit 1' + sql coordinator1a { + SET citus.shard_count TO 4; CREATE TABLE t1 (a int); SELECT + create_distributed_table('t1', 'a'); + } +} + +step test_004_fail_while_transaction_is_in_progress { + network disconnect worker1a + wait until worker1b state is wait_primary timeout 90s +} + +# +# test_005: drop failed primary worker2a and coordinator1a +# + +step test_005_drop_primary_worker { + network disconnect worker2a + wait until worker2b state is wait_primary timeout 90s + compose stop worker2a + exec monitor psql -d pg_auto_failover -tAc "SELECT pgautofailover.remove_node(nodehost, nodeport) FROM pgautofailover.node WHERE nodename = 'worker2a'" + wait until worker2b state is single timeout 60s +} + +step test_005_drop_primary_coordinator { + compose stop coordinator1a + exec monitor psql -d pg_auto_failover -tAc "SELECT pgautofailover.remove_node(nodehost, nodeport) FROM pgautofailover.node WHERE nodename = 'coordinator1a'" + wait until coordinator1b state is single timeout 60s +} diff --git a/tests/tap/specs/citus_multi_standbys.pgaf b/tests/tap/specs/citus_multi_standbys.pgaf new file mode 100644 index 000000000..b81b51c69 --- /dev/null +++ b/tests/tap/specs/citus_multi_standbys.pgaf @@ -0,0 +1,176 @@ +# Test Citus cluster with multiple standbys per group (3 nodes per group: +# a primary, a sync secondary, and an async secondary with candidate-priority 0 +# and replication-quorum false). Verifies failover, data consistency, and +# synchronous_standby_names after each failure/recovery cycle. +# +# Ported from tests/test_citus_multi_standbys.py +# Predecessor: tests/test_citus_multi_standbys.py + +cluster { + monitor + formation { + coordinator1a coordinator + coordinator1b coordinator + node coordinator1c { + coordinator + candidate-priority 0 + replication-quorum false + } + worker1a worker group 1 + worker1b worker group 1 + node worker1c { + worker group 1 + candidate-priority 0 + replication-quorum false + } + worker2a worker group 2 + worker2b worker group 2 + node worker2c { + worker group 2 + candidate-priority 0 + replication-quorum false + } + } +} + +setup { + wait until primary, secondary in group 0 timeout 90s + wait until primary, secondary in group 1 timeout 90s + wait until primary, secondary in group 2 timeout 90s + wait until coordinator1c state is secondary timeout 90s + wait until worker1c state is secondary timeout 90s + wait until worker2c state is secondary timeout 90s + promote coordinator1a, worker1a, worker2a +} + +teardown { + compose down +} + +# +# test_001: coordinator group with async secondary coordinator1c +# + +step test_001_init_coordinator { + wait until coordinator1a state is primary + and coordinator1b state is secondary + and coordinator1c state is secondary + timeout 90s + exec coordinator1c pg_autoctl get node candidate-priority + expect { 0 } +} + +# +# test_002: worker groups with async standbys +# + +step test_002_init_workers { + wait until worker1a state is primary + and worker1b state is secondary + and worker1c state is secondary + and worker2a state is primary + and worker2b state is secondary + and worker2c state is secondary + timeout 90s + exec worker1c pg_autoctl get node candidate-priority + expect { 0 } + exec worker2c pg_autoctl get node candidate-priority + expect { 0 } +} + +# +# test_003: create distributed table and seed data +# + +step test_003_001_create_distributed_table { + exec coordinator1a sh -c 'for i in $(seq 1 30); do n=$(psql -U docker -d demo -tAc "SELECT count(*) FROM pg_dist_node WHERE metadatasynced = false AND isactive = true"); [ "$n" = "0" ] && exit 0; sleep 2; done; exit 1' + sql coordinator1a { ALTER DATABASE demo SET citus.shard_count TO 4; } + sql coordinator1a { CREATE TABLE t1 (a int); } + sql coordinator1a { SELECT create_distributed_table('t1', 'a'); } + sql coordinator1a { INSERT INTO t1 VALUES (1), (2); } + sql coordinator1a { SELECT a FROM t1 ORDER BY a; } + expect { { 1 } { 2 } } +} + +# +# test_004: fail worker2a, reads for worker1 still work via coordinator +# + +step test_004_001_fail_worker2 { + network disconnect worker2a +} + +step test_004_002_writes_via_coordinator_to_worker2_fail { + sql coordinator1a { INSERT INTO t1 VALUES (3); } + expect error +} + +step test_004_004_wait_for_failover { + wait until worker2b state is wait_primary + and worker2c state is secondary + timeout 90s + exec worker2c pg_autoctl get node candidate-priority + expect { 0 } +} + +step test_004_005_writes_via_coordinator_succeed_after_failover { + sql coordinator1a { INSERT INTO t1 VALUES (3); } +} + +step test_005_read_from_workers_via_coordinator { + sql coordinator1a { SELECT a FROM t1 ORDER BY a ASC; } + expect { { 1 } { 2 } { 3 } } +} + +# +# test_006: write more data +# + +step test_006_writes_to_coordinator_succeed { + sql coordinator1a { INSERT INTO t1 VALUES (4); } + sql coordinator1a { SELECT a FROM t1 ORDER BY a ASC; } + expect { { 1 } { 2 } { 3 } { 4 } } +} + +# +# test_007: restart worker2a as secondary under new primary worker2b +# + +step test_007_start_worker2a_again { + network connect worker2a + wait until worker2a state is secondary + and worker2b state is primary + timeout 90s +} + +step test_008_read_from_workers_via_coordinator { + sql coordinator1a { SELECT a FROM t1 ORDER BY a ASC; } + expect { { 1 } { 2 } { 3 } { 4 } } +} + +# +# test_009: fail worker2b, failover back to worker2a +# + +step test_009_fail_worker2b { + network disconnect worker2b + wait until worker2a state is wait_primary + and worker2c state is secondary + timeout 90s +} + +step test_010_read_from_workers_via_coordinator { + sql coordinator1a { SELECT a FROM t1 ORDER BY a ASC; } + expect { { 1 } { 2 } { 3 } { 4 } } +} + +# +# test_011: restart worker2b as secondary +# + +step test_011_start_worker2b_again { + network connect worker2b + wait until worker2b state is secondary + and worker2a state is primary + timeout 90s +} diff --git a/tests/tap/specs/citus_skip_pg_hba.pgaf b/tests/tap/specs/citus_skip_pg_hba.pgaf new file mode 100644 index 000000000..055aa64dc --- /dev/null +++ b/tests/tap/specs/citus_skip_pg_hba.pgaf @@ -0,0 +1,124 @@ +# Test Citus cluster with authMethod=skip (pg_autoctl does not edit pg_hba.conf). +# +# With auth=skip, pg_autoctl leaves pg_hba.conf untouched after postgres init. +# The coordinator pair starts normally (coord0b pg_basebackup-s from coord0a +# and inherits its HBA; the monitor and coordinator connections work via the +# Docker network with trust auth set at the monitor level). +# +# worker1a is launch deferred. The first manual run fails because the +# coordinator tries master_activate_node on worker1a but worker1a's default +# pg_hba.conf blocks that connection. After we manually append the required +# HBA rules and retry, activation succeeds — and pg_autoctl has not modified +# pg_hba.conf itself (verified by assert hba-edited = false). +# +# worker1b is also launch deferred to ensure it starts only after worker1a is +# the active primary for group 1, matching the Python test's sequential order. +# +# Ported from tests/test_citus_skip_pg_hba.py +# Predecessor: tests/test_citus_skip_pg_hba.py + +cluster { + monitor + auth skip + formation { + coord0a coordinator + coord0b coordinator launch deferred + worker1a worker group 1 launch deferred + worker1b worker group 1 launch deferred + } +} + +setup { + exec monitor pg_autoctl inspect pgsetup wait +} + +teardown { + compose down +} + +# +# test_000: monitor with auth=skip, append trust rule to pg_hba.conf manually +# + +step test_000_create_monitor { + exec monitor pg_autoctl override pgsetup hba-lan + exec coord0a pg_autoctl override pgsetup hba-lan +} + +# +# test_001a: init coordinator coord0a (single) +# + +step test_001a_init_coordinator { + wait until coord0a state is single timeout 60s +} + +# +# test_001b: init coordinator coord0b (secondary) +# + +step test_001b_init_coordinator { + exec coord0b pg_autoctl node start /etc/pgaf/node.ini + wait until coord0a state is primary + and coord0b state is secondary + timeout 90s +} + +step test_002b_create_worker { + exec-fails worker1a pg_autoctl node run /etc/pgaf/node.ini +} + +# +# test_002b/002d: worker1a — registration initially fails because coordinator +# cannot connect; after manually adding HBA rules worker1a runs and activates. +# + +step test_002d_run_worker { + exec worker1a pg_autoctl override pgsetup hba-lan + exec worker1a pg_autoctl node run --background /etc/pgaf/node.ini +} + +step test_002e_check_hba { + exec-fails worker1a grep "Auto-generated by pg_auto_failover" /var/lib/postgres/pgaf/pg_hba.conf +} + +step test_002f_activated_node { + sql coord0a { + SELECT isactive FROM pg_dist_node WHERE nodename = 'worker1a'; + } + expect { true } +} + +# +# test_003: init worker1b as secondary +# + +step test_003_init_worker { + exec worker1b pg_autoctl node run --background /etc/pgaf/node.ini + wait until worker1a state is primary + and worker1b state is secondary + timeout 90s + exec-fails worker1b grep "Auto-generated by pg_auto_failover" /var/lib/postgres/pgaf/pg_hba.conf +} + +# +# test_004: create distributed table +# + +step test_004_create_distributed_table { + sql coord0a { CREATE TABLE t1 (a int); } + sql coord0a { SELECT create_distributed_table('t1', 'a'); } + sql coord0a { INSERT INTO t1 VALUES (1), (2); } +} + +# +# test_005: manual failover of worker group 1 +# + +step test_005_failover { + exec monitor pg_autoctl perform failover --group 1 + wait until worker1b state is wait_primary timeout 90s + wait until worker1a state is secondary + and worker1b state is primary + timeout 90s +} diff --git a/tests/tap/specs/config_get_set.pgaf b/tests/tap/specs/config_get_set.pgaf new file mode 100644 index 000000000..0374e05e9 --- /dev/null +++ b/tests/tap/specs/config_get_set.pgaf @@ -0,0 +1,48 @@ +# Test pg_autoctl config get / config set behaviour on both the monitor and a +# keeper node, including validation (rejecting invalid values) and no +# unintended side-effects on other settings. +# +# Ported from tests/test_config_get_set.py +# Predecessor: tests/test_config_get_set.py + +cluster { + monitor + formation { + node1 + } +} + +setup { + wait until node1 state is single timeout 60s +} + +teardown { + compose down +} + +step test_001_init_primary { + exec node1 pg_autoctl set node metadata --name "node a" + exec node1 pg_autoctl show state + exec node1 pg_autoctl config set pg_autoctl.name a + sleep 2s + exec node1 pg_autoctl show settings +} + +step test_002_config_set_monitor { + exec monitor pg_autoctl config set ssl.sslmode prefer + exec monitor pg_autoctl config get postgresql.pg_ctl + expect { /usr/lib/postgresql/17/bin/pg_ctl } + exec-fails monitor pg_autoctl config set postgresql.pg_ctl invalid + exec monitor pg_autoctl config get postgresql.pg_ctl + expect { /usr/lib/postgresql/17/bin/pg_ctl } + exec monitor pg_autoctl config get ssl.sslmode + expect { prefer } +} + +step test_002b_config_set_node { + exec node1 pg_autoctl config get postgresql.pg_ctl + expect { /usr/lib/postgresql/17/bin/pg_ctl } + exec-fails node1 pg_autoctl config set postgresql.pg_ctl invalid + exec node1 pg_autoctl config get postgresql.pg_ctl + expect { /usr/lib/postgresql/17/bin/pg_ctl } +} diff --git a/tests/tap/specs/create_standby_with_pgdata.pgaf b/tests/tap/specs/create_standby_with_pgdata.pgaf new file mode 100644 index 000000000..61699c624 --- /dev/null +++ b/tests/tap/specs/create_standby_with_pgdata.pgaf @@ -0,0 +1,80 @@ +# Test creating a standby from an existing PGDATA directory. +# +# test_003/004/005: verify that registering a node whose PGDATA was created by +# a separate initdb (different system_identifier) is rejected by the monitor. +# +# test_006: start node3 via pg_autoctl node start; pg_autoctl detects the +# pre-existing pg_basebackup PGDATA and sets up streaming replication without +# running another pg_basebackup (skipBaseBackup path in fsm_init_standby). +# +# Both node2 and node3 are declared launch deferred: their containers sleep +# until a step explicitly calls pg_autoctl node start (which rewrites the ini +# from mode=deferred to mode=immediate and unblocks the waiting node run loop). +# +# Ported from tests/test_create_standby_with_pgdata.py +# Predecessor: tests/test_create_standby_with_pgdata.py + +cluster { + monitor + formation { + node1 + node2 launch deferred + node3 launch deferred + } +} + +setup { + wait until node1 state is single timeout 60s + exec node1 pg_autoctl inspect pgsetup wait +} + +teardown { + compose down +} + +# +# test_001: init primary +# + +step test_001_init_primary { + wait until node1 state is single timeout 60s +} + +# +# test_002: create table t1 +# + +step test_002_create_t1 { + sql node1 { CREATE TABLE t1(a int); } + sql node1 { INSERT INTO t1 VALUES (1), (2); } +} + +step test_003_init_secondary { + exec node2 pg_ctl initdb -D /var/lib/postgres/pgaf +} + +step test_004_create_raises_error { + exec-fails node2 pg_autoctl create postgres --pgdata /var/lib/postgres/pgaf --monitor postgresql://autoctl_node@monitor/pg_auto_failover --auth trust --ssl-self-signed --name node2 --hostname node2 +} + +step test_005_cleanup_after_failure { + exec node2 rm -rf /var/lib/postgres/pgaf +} + +step test_006_init_secondary { + exec node3 pg_autoctl node start /etc/pgaf/node.ini + wait until node3 state is secondary + and node1 state is primary + timeout 90s +} + +# +# test_007: manual failover to node3 +# + +step test_007_failover { + exec monitor pg_autoctl perform failover + wait until node3 state is primary + and node1 state is secondary + timeout 90s +} diff --git a/tests/tap/specs/debian_clusters.pgaf b/tests/tap/specs/debian_clusters.pgaf new file mode 100644 index 000000000..950a7310c --- /dev/null +++ b/tests/tap/specs/debian_clusters.pgaf @@ -0,0 +1,51 @@ +# Test pg_auto_failover with Debian-style pg_createcluster layouts where the +# postgresql.conf lives outside PGDATA (in /etc/postgresql///). +# pg_autoctl must detect and support this split-config layout. +# +# Ported from tests/test_debian_clusters.py +# Predecessor: tests/test_debian_clusters.py + +cluster { + monitor + formation { + node1 debian-cluster main + } +} + +setup { + wait until node1 state is single timeout 60s +} + +teardown { + compose down +} + +# +# test_001: pg_autoctl adopts the Debian "main" cluster pre-created by +# pg_createcluster at image build time. postgresql.conf starts outside +# PGDATA (/etc/postgresql/17/main/); pg_autoctl moves it in on first run. +# + +step test_001_single_with_debian_cluster { + exec monitor pg_autoctl inspect pgsetup wait + exec node1 pg_autoctl inspect pgsetup wait +} + +# +# test_002: verify pg_autoctl moved postgresql.conf into PGDATA. +# + +step test_002_conf_in_pgdata { + exec node1 test -f /var/lib/postgresql/17/main/postgresql.conf +} + +# +# test_003: basic write/read to confirm Postgres is functional. +# + +step test_003_write_and_read { + sql node1 { CREATE TABLE t1 (a int); } + sql node1 { INSERT INTO t1 VALUES (1), (2), (3); } + sql node1 { SELECT * FROM t1 ORDER BY a; } + expect { { 1 } { 2 } { 3 } } +} diff --git a/tests/tap/specs/enable_ssl.pgaf b/tests/tap/specs/enable_ssl.pgaf new file mode 100644 index 000000000..d03d7440d --- /dev/null +++ b/tests/tap/specs/enable_ssl.pgaf @@ -0,0 +1,84 @@ +# Test enabling SSL on a running cluster: nodes start without SSL and SSL is +# enabled live with --ssl-self-signed. After enabling, replication must still +# work and reads from the secondary must return correct data. +# +# CA-signed certificate tests (verify-ca / verify-full) are covered separately +# in ssl_cert.pgaf, which starts the cluster with SSL already configured. +# +# Ported from tests/test_enable_ssl.py +# Predecessor: tests/test_enable_ssl.py + +cluster { + monitor + ssl off + formation { + node1 + node2 + } +} + +setup { + wait until primary, secondary timeout 60s + promote node1 +} + +teardown { + compose down +} + +step test_001_init_primary { + sql monitor { SHOW ssl; } + expect { off } + sql node1 { SHOW ssl; } + expect { off } + sql node2 { SHOW ssl; } + expect { off } +} + +step test_002_create_t1 { + sql node1 { CREATE TABLE t1(a int); } + sql node1 { INSERT INTO t1 VALUES (1), (2); } +} + +step test_004_maintenance { + exec node2 pg_autoctl enable maintenance + wait until node2 state is maintenance timeout 60s +} + +step test_005_enable_ssl_monitor { + exec monitor pg_autoctl enable ssl --ssl-self-signed --ssl-mode require + exec monitor pg_autoctl inspect pgsetup wait --timeout 60 + sql monitor { SHOW ssl; } + expect { on } +} + +step test_006_enable_ssl_primary { + exec node1 pg_autoctl enable ssl --ssl-self-signed --ssl-mode require + exec node1 pg_autoctl inspect pgsetup wait --timeout 60 + sql node1 { SHOW ssl; } + expect { on } +} + +step test_007_enable_ssl_secondary { + exec node2 pg_autoctl enable ssl --ssl-self-signed --ssl-mode require + compose stop node2 + compose start node2 + exec node2 pg_autoctl inspect pgsetup wait --timeout 90 + sql node2 { SHOW ssl; } + expect { on } +} + +step test_008_disable_maintenance { + exec node2 pg_autoctl disable maintenance + wait until node2 state is secondary + and node1 state is primary + timeout 90s +} + +step test_009_read_from_secondary { + exec node2 pg_autoctl inspect pgsetup wait + sql node2 { SHOW ssl; } + expect { on } + sql node2 { SELECT * FROM t1 ORDER BY a; } + expect { { 1 } { 2 } } +} diff --git a/tests/tap/specs/ensure.pgaf b/tests/tap/specs/ensure.pgaf new file mode 100644 index 000000000..9020fa7a9 --- /dev/null +++ b/tests/tap/specs/ensure.pgaf @@ -0,0 +1,69 @@ +# Test pg_autoctl "ensure" behaviour: verify that the keeper restarts +# Postgres after an unexpected stop, survives a demoted transition, and +# correctly orchestrates a failover when Postgres is broken on the primary. +# +# node2 is declared launch deferred so it starts in single mode for test_001, +# matching the Python test order (node2 is not created until test_003). +# +# Ported from tests/test_ensure.py +# Predecessor: tests/test_ensure.py + +cluster { + monitor + formation { + node1 + node2 launch deferred + } +} + +setup { + wait until node1 state is single timeout 60s + exec node1 pg_autoctl inspect pgsetup wait +} + +teardown { + compose down +} + +step test_001_init_primary { + stop postgres node1 + wait until node1 state is single timeout 90s + exec node1 pg_autoctl inspect pgsetup wait +} + +step test_002_create_t1 { + sql node1 { CREATE TABLE t1(a int); } + sql node1 { INSERT INTO t1 VALUES (1), (2); } +} + +step test_003_init_secondary { + exec node2 pg_autoctl node start + wait until node2 state is secondary + and node1 state is primary + timeout 90s + stop postgres node2 + wait until node2 state is secondary + and node1 state is primary + timeout 90s +} + +step test_004_demoted { + compose stop node1 + sleep 30s + compose start node1 + wait until node1 state is demoted timeout 120s + wait until node2 state is primary + and node1 state is secondary + timeout 120s +} + +step test_005_inject_error_in_node2 { + wait until node2 state is primary timeout 60s + wait until node2 state is primary timeout 90s + exec node2 bash -c "echo \"shared_preload_libraries='wrong_extension'\" >> /var/lib/postgres/pgaf/postgresql.conf" + stop postgres node2 + wait until node1 state is wait_primary timeout 120s + wait until node2 state is secondary + and node1 state is primary + timeout 120s +} diff --git a/tests/tap/specs/extension_update.pgaf b/tests/tap/specs/extension_update.pgaf new file mode 100644 index 000000000..921cf3dca --- /dev/null +++ b/tests/tap/specs/extension_update.pgaf @@ -0,0 +1,37 @@ +# Test pg_auto_failover extension version management. +# The monitor starts with a "dummy" extension version baked into the Docker +# image. pg_autoctl detects the mismatch at startup, runs ALTER EXTENSION +# pgautofailover UPDATE, and restarts Postgres. The test verifies that the +# installed_version seen in pg_available_extensions matches the dummy version +# and that the monitor comes back healthy after the restart. +# +# Predecessor: tests/test_extension_update.py + +cluster { + monitor + extension-version "dummy" +} + +setup { + exec monitor pg_autoctl inspect pgsetup wait +} + +teardown { + compose down +} + +# +# test_001: start monitor with a dummy extension version and verify +# that the installed version matches the dummy version. +# + +step test_001_update_extension { + sleep 3s + exec monitor pg_autoctl inspect pgsetup wait + sql monitor { + SELECT installed_version + FROM pg_available_extensions + WHERE name = 'pgautofailover'; + } + expect { dummy } +} diff --git a/tests/tap/specs/installcheck.pgaf b/tests/tap/specs/installcheck.pgaf new file mode 100644 index 000000000..ce8b9005a --- /dev/null +++ b/tests/tap/specs/installcheck.pgaf @@ -0,0 +1,46 @@ +# Test the pgautofailover SQL extension via pg_regress installcheck. +# +# The monitor uses the "testrun" Dockerfile target which includes the full source +# tree and postgresql-server-dev (providing pg_regress). pg_regress connects +# to the running monitor Postgres from within the same container. +# +# Coverage mirrors tests/test_installcheck.py: +# - monitor created and running +# - pg_hba.conf widened so pg_regress can connect (host all all 0.0.0.0/0) +# - src/monitor writable for pg_regress results output +# - make installcheck runs all SQL regression tests in src/monitor/ +# +# Ported from tests/test_installcheck.py +# Predecessor: tests/test_installcheck.py + +cluster { + monitor + image "pgaf:testrun" +} + +setup { + exec monitor pg_autoctl inspect pgsetup wait +} + +teardown { + compose down +} + +# +# test_001: append a trust HBA entry so pg_regress can connect from localhost. +# + +step test_001_add_hba_entry { + exec monitor bash -c "echo 'host all all %CIDR% trust' >> /var/lib/postgres/pgaf/pg_hba.conf" + exec monitor pg_autoctl reload +} + +# +# test_002: make src/monitor writable for pg_regress results output, +# then run all regression SQL tests. +# + +step test_002_make_installcheck { + exec monitor sudo chmod -R go+w /usr/src/pg_auto_failover/src/monitor + exec monitor make -C /usr/src/pg_auto_failover/src/monitor installcheck PGHOST=127.0.0.1 +} diff --git a/tests/tap/specs/maintenance_and_drop.pgaf b/tests/tap/specs/maintenance_and_drop.pgaf new file mode 100644 index 000000000..b933b5035 --- /dev/null +++ b/tests/tap/specs/maintenance_and_drop.pgaf @@ -0,0 +1,75 @@ +# Test maintenance mode and clean node drop: enable maintenance with a +# concurrent node failure, verify the cluster recovers when maintenance is +# disabled, then simulate a primary failure, recover, and finally drop a +# node cleanly verifying the formation returns to single. +# +# Predecessor: tests/test_create_run.py + +cluster { + monitor + formation { + node1 + node2 + } +} + +setup { + wait until primary, secondary timeout 90s + promote node1 +} + +teardown { + compose down +} + +step test_002_create_t1 { + sql node1 { CREATE TABLE t1(a int); } + sql node1 { INSERT INTO t1 VALUES (1), (2); } +} + +step test_004_read_from_secondary { + sql node2 { SELECT * FROM t1 ORDER BY a; } + expect { { 1 } { 2 } } +} + +step test_005_maintenance { + exec node2 pg_autoctl enable maintenance + wait until node2 state is maintenance timeout 60s + sql node1 { INSERT INTO t1 VALUES (3); } + exec node2 pg_autoctl disable maintenance + wait until node2 state is secondary + and node1 state is primary + timeout 90s +} + +step test_006_fail_primary { + compose stop node1 + wait until node2 state is wait_primary timeout 180s +} + +step test_007_start_node1_again { + compose start node1 + wait until node2 state is primary + and node1 state is secondary + timeout 90s +} + +step test_008_read_from_new_secondary { + sql node1 { SELECT * FROM t1 ORDER BY a; } + expect { { 1 } { 2 } { 3 } } +} + +step test_009_fail_secondary { + compose stop node1 + wait until node2 state is wait_primary timeout 90s +} + +step test_010_drop_secondary { + compose start node1 + wait until node2 state is primary + and node1 state is secondary + timeout 90s + exec node1 pg_autoctl drop node --no-wait + wait until node1 stopped timeout 30s + wait until node2 state is single timeout 60s +} diff --git a/tests/tap/specs/monitor_disabled.pgaf b/tests/tap/specs/monitor_disabled.pgaf new file mode 100644 index 000000000..4ebacca3b --- /dev/null +++ b/tests/tap/specs/monitor_disabled.pgaf @@ -0,0 +1,98 @@ +# Test operating pg_autoctl without a monitor ("monitor disabled" mode). +# The FSM is driven manually via pg_autoctl manual fsm assign / nodes set. +# +# No-monitor nodes start pg_autoctl node run automatically (like all nodes) +# but with no_monitor = true in the ini, so there is no monitor registration. +# Each test step drives the FSM manually. +# +# Ported from tests/test_monitor_disabled.py +# Predecessor: tests/test_monitor_disabled.py + +cluster { + ssl off + formation { + node1 no-monitor + node2 no-monitor + node3 no-monitor + } +} + +# No monitor, so no automatic state convergence — each step drives the FSM +# manually. + +setup { + # No monitor: nodes start pg_autoctl in INIT state (postgres not yet + # running). The first test step assigns SINGLE state to node1 which + # starts postgres. Just give pg_autoctl a moment to initialize the + # data directory before the test steps begin. + sleep 5s +} + +teardown { + compose down +} + +step test_002_init_to_single { + exec node1 pg_autoctl manual fsm assign single + exec node1 pg_autoctl inspect pgsetup wait +} + +step test_003_create_t1 { + sql node1 { CREATE TABLE t1(a int); } + sql node1 { INSERT INTO t1 VALUES (1), (2); } +} + +step test_004_init_secondary { + sleep 5s +} + +step test_005_fsm_nodes_set { + exec node1 sh -c 'printf '"'"'[{"node_id":1,"node_name":"node1","node_host":"node1","node_port":5432,"node_cluster":"default","node_lsn":"0/1","node_is_primary":true},{"node_id":2,"node_name":"node2","node_host":"node2","node_port":5432,"node_cluster":"default","node_lsn":"0/1","node_is_primary":false}]'"'"' > /tmp/nodes12.json && pg_autoctl manual fsm nodes set /tmp/nodes12.json' + exec node2 sh -c 'printf '"'"'[{"node_id":1,"node_name":"node1","node_host":"node1","node_port":5432,"node_cluster":"default","node_lsn":"0/1","node_is_primary":true},{"node_id":2,"node_name":"node2","node_host":"node2","node_port":5432,"node_cluster":"default","node_lsn":"0/1","node_is_primary":false}]'"'"' > /tmp/nodes12.json && pg_autoctl manual fsm nodes set /tmp/nodes12.json' +} + +step test_006_init_to_wait_standby { + exec node2 pg_autoctl manual fsm assign wait_standby +} + +step test_007_catchingup { + exec node1 pg_autoctl manual fsm assign wait_primary + exec node2 pg_autoctl manual fsm assign catchingup +} + +step test_008_secondary { + exec node1 pg_autoctl manual fsm assign primary + exec node2 pg_autoctl manual fsm assign secondary + sql node1 { SHOW synchronous_standby_names; } + expect { * } +} + +step test_009_init_secondary { + sleep 5s +} + +step test_010_fsm_nodes_set { + exec node1 sh -c 'printf '"'"'[{"node_id":1,"node_name":"node1","node_host":"node1","node_port":5432,"node_cluster":"default","node_lsn":"0/1","node_is_primary":true},{"node_id":2,"node_name":"node2","node_host":"node2","node_port":5432,"node_cluster":"default","node_lsn":"0/1","node_is_primary":false},{"node_id":3,"node_name":"node3","node_host":"node3","node_port":5432,"node_cluster":"default","node_lsn":"0/1","node_is_primary":false}]'"'"' > /tmp/nodes123.json && pg_autoctl manual fsm nodes set /tmp/nodes123.json' + exec node2 sh -c 'printf '"'"'[{"node_id":1,"node_name":"node1","node_host":"node1","node_port":5432,"node_cluster":"default","node_lsn":"0/1","node_is_primary":true},{"node_id":2,"node_name":"node2","node_host":"node2","node_port":5432,"node_cluster":"default","node_lsn":"0/1","node_is_primary":false},{"node_id":3,"node_name":"node3","node_host":"node3","node_port":5432,"node_cluster":"default","node_lsn":"0/1","node_is_primary":false}]'"'"' > /tmp/nodes123.json && pg_autoctl manual fsm nodes set /tmp/nodes123.json' + exec node3 sh -c 'printf '"'"'[{"node_id":1,"node_name":"node1","node_host":"node1","node_port":5432,"node_cluster":"default","node_lsn":"0/1","node_is_primary":true},{"node_id":2,"node_name":"node2","node_host":"node2","node_port":5432,"node_cluster":"default","node_lsn":"0/1","node_is_primary":false},{"node_id":3,"node_name":"node3","node_host":"node3","node_port":5432,"node_cluster":"default","node_lsn":"0/1","node_is_primary":false}]'"'"' > /tmp/nodes123.json && pg_autoctl manual fsm nodes set /tmp/nodes123.json' +} + +step test_011_init_to_wait_standby { + exec node1 pg_autoctl manual fsm assign primary + exec node3 pg_autoctl manual fsm assign wait_standby + sql node1 { SHOW synchronous_standby_names; } + expect { * } +} + +step test_012_catchingup { + exec node3 pg_autoctl manual fsm assign catchingup + sql node1 { SHOW synchronous_standby_names; } + expect { * } +} + +step test_013_secondary { + exec node3 pg_autoctl manual fsm assign secondary + exec node1 pg_autoctl manual fsm assign primary + sql node1 { SHOW synchronous_standby_names; } + expect { * } +} diff --git a/tests/tap/specs/multi_alternate.pgaf b/tests/tap/specs/multi_alternate.pgaf new file mode 100644 index 000000000..5d1ba9d9c --- /dev/null +++ b/tests/tap/specs/multi_alternate.pgaf @@ -0,0 +1,267 @@ +# Test alternating failover scenarios with three nodes: each node serves as +# primary at least once across three series. Series 003 kills node1 then +# node2 in sequence; series 005 reverses the restart order; series 006 kills +# node3 (the surviving primary) and concurrently restarts a previously failed +# node. Verifies correct election outcomes and that pg_rewind restores each +# node as a healthy secondary after it rejoins. +# +# All kills use SIGKILL (compose kill) so the stopped process has no chance +# to update the monitor or claim a re-election win during shutdown. +# +# Predecessor: tests/test_multi_alternate_primary_failures.py + +cluster { + monitor + ssl off + formation { + node1 + node2 + node3 + } +} + +setup { + wait until primary, secondary timeout 120s + wait until node3 state is secondary timeout 60s + promote node1 + wait until node1 state is primary + and node2 state is secondary + and node3 state is secondary + timeout 120s +} + +teardown { + compose down +} + +# +# test_002: verify replication slots after initial 3-node convergence. +# Node ids are assigned in registration order: node1=1, node2=2, node3=3. +# Each primary holds physical replication slots for each standby. +# After setup node1 is primary; it holds slots for standbys 2 and 3. +# node2 and node3 are standbys; each holds a slot for the other two. +# + +step test_002_check_initial_slots { + sql node1 { + SELECT slot_name + FROM pg_replication_slots + WHERE slot_name ~ '^pgautofailover_standby_' AND slot_type = 'physical' + ORDER BY slot_name; + } + expect { { pgautofailover_standby_2 } { pgautofailover_standby_3 } } + sql node2 { + SELECT slot_name + FROM pg_replication_slots + WHERE slot_name ~ '^pgautofailover_standby_' AND slot_type = 'physical' + ORDER BY slot_name; + } + expect { { pgautofailover_standby_1 } { pgautofailover_standby_3 } } + sql node3 { + SELECT slot_name + FROM pg_replication_slots + WHERE slot_name ~ '^pgautofailover_standby_' AND slot_type = 'physical' + ORDER BY slot_name; + } + expect { { pgautofailover_standby_1 } { pgautofailover_standby_2 } } +} + +# +# Series 003: stop node1 (primary) → node2 becomes primary, +# then stop node2 → node3 reports_lsn and waits, +# restart node2 → node2 becomes primary again, +# restart node1 → node1 becomes secondary. +# +# node1 node2 node3 +# primary secondary secondary +# +# demoted primary secondary +# +# demoted draining report_lsn +# +# demoted primary secondary +# +# secondary primary secondary +# + +step test_003_001_stop_primary { + compose kill node1 + wait until node2 state is primary + and node3 state is secondary + timeout 120s +} + +step test_003_002_stop_primary { + compose kill node2 + wait until node2 assigned-state = draining timeout 120s + wait until node3 state is report_lsn timeout 120s +} + +step test_003_003_bringup_last_failed_primary { + compose start node2 + wait until node2 state is primary + and node3 state is secondary + timeout 180s +} + +step test_003_004_bringup_first_failed_primary { + compose start node1 + wait until node1 assigned-state = secondary timeout 90s + wait until node2 assigned-state = primary timeout 90s + wait until node3 assigned-state = secondary timeout 90s +} + +# After series 003: node2 is primary (id=2), node1 and node3 are standbys. + +step test_003_005_check_slots_after_series_003 { + sql node2 { + SELECT slot_name + FROM pg_replication_slots + WHERE slot_name ~ '^pgautofailover_standby_' AND slot_type = 'physical' + ORDER BY slot_name; + } + expect { { pgautofailover_standby_1 } { pgautofailover_standby_3 } } + sql node1 { + SELECT slot_name + FROM pg_replication_slots + WHERE slot_name ~ '^pgautofailover_standby_' AND slot_type = 'physical' + ORDER BY slot_name; + } + expect { { pgautofailover_standby_2 } { pgautofailover_standby_3 } } + sql node3 { + SELECT slot_name + FROM pg_replication_slots + WHERE slot_name ~ '^pgautofailover_standby_' AND slot_type = 'physical' + ORDER BY slot_name; + } + expect { { pgautofailover_standby_1 } { pgautofailover_standby_2 } } +} + +step test_005_001_fail_primary_again { + compose kill node2 + wait until node1 state is primary + and node3 state is secondary + timeout 180s +} + +step test_005_001b_write_and_checkpoint { + wait until node1 state is primary + and node3 state is secondary + timeout 120s + sql node1 { + CREATE TABLE IF NOT EXISTS _lsn_advance(x int); INSERT INTO + _lsn_advance VALUES(1); CHECKPOINT; + } +} + +step test_005_002_fail_primary_again { + compose kill node1 + wait until node3 assigned-state = report_lsn timeout 120s +} + +step test_005_003_bring_up_first_failed_primary { + compose start node2 + wait until node2 state is demoted timeout 120s + wait until node2 state is secondary + and node3 state is primary + timeout 120s +} + +step test_005_004_bring_up_last_failed_primary { + compose start node1 + wait until node1 state is secondary + and node3 state is primary + timeout 120s +} + +# +# Series 006: stop node3 (primary) → node1 becomes primary, +# stop node1 and concurrently restart node3, +# node2 becomes primary, node3 becomes secondary, +# restart node1 → node1 becomes secondary. +# +# node1 node2 node3 +# secondary secondary primary +# +# primary secondary demoted +# +# +# demoted primary secondary +# +# secondary primary secondary +# + +# After series 005: node3 is primary (id=3), node1 and node2 are standbys. + +step test_005_005_check_slots_after_series_005 { + sql node3 { + SELECT slot_name + FROM pg_replication_slots + WHERE slot_name ~ '^pgautofailover_standby_' AND slot_type = 'physical' + ORDER BY slot_name; + } + expect { { pgautofailover_standby_1 } { pgautofailover_standby_2 } } + sql node1 { + SELECT slot_name + FROM pg_replication_slots + WHERE slot_name ~ '^pgautofailover_standby_' AND slot_type = 'physical' + ORDER BY slot_name; + } + expect { { pgautofailover_standby_2 } { pgautofailover_standby_3 } } + sql node2 { + SELECT slot_name + FROM pg_replication_slots + WHERE slot_name ~ '^pgautofailover_standby_' AND slot_type = 'physical' + ORDER BY slot_name; + } + expect { { pgautofailover_standby_1 } { pgautofailover_standby_3 } } +} + +step test_006_001_fail_primary { + compose kill node3 + wait until node1 state is primary + and node2 state is secondary + timeout 120s +} + +step test_006_002_fail_new_primary { + compose kill node1 + compose start node3 + wait until node2 state is primary + and node3 state is secondary + timeout 120s +} + +step test_006_003_bringup_last_failed_primary { + compose start node1 + wait until node1 state is secondary + and node2 state is primary + and node3 state is secondary + timeout 120s +} + +# After series 006: node2 is primary (id=2), node1 and node3 are standbys. + +step test_006_004_check_slots_after_series_006 { + sql node2 { + SELECT slot_name + FROM pg_replication_slots + WHERE slot_name ~ '^pgautofailover_standby_' AND slot_type = 'physical' + ORDER BY slot_name; + } + expect { { pgautofailover_standby_1 } { pgautofailover_standby_3 } } + sql node1 { + SELECT slot_name + FROM pg_replication_slots + WHERE slot_name ~ '^pgautofailover_standby_' AND slot_type = 'physical' + ORDER BY slot_name; + } + expect { { pgautofailover_standby_2 } { pgautofailover_standby_3 } } + sql node3 { + SELECT slot_name + FROM pg_replication_slots + WHERE slot_name ~ '^pgautofailover_standby_' AND slot_type = 'physical' + ORDER BY slot_name; + } + expect { { pgautofailover_standby_1 } { pgautofailover_standby_2 } } +} diff --git a/tests/tap/specs/multi_async.pgaf b/tests/tap/specs/multi_async.pgaf new file mode 100644 index 000000000..d705d6fcb --- /dev/null +++ b/tests/tap/specs/multi_async.pgaf @@ -0,0 +1,251 @@ +# Test mixed sync/async replication configurations with four nodes. +# Starts with one async node (node3), then exercises: setting the whole +# formation async, async failover via perform-promotion, returning to mixed +# sync/async, dropping a node, and two double-failure scenarios (tests 014 +# and 015) where both the primary and the new primary fail before recovery. +# Also covers: candidate-priority=0 secondary rejoining while primary is +# wait_primary, and async standby failing while primary is already wait_primary. +# +# Predecessor: tests/test_multi_async.py + +cluster { + monitor + ssl off + formation { + node1 + node2 + node3 replication-quorum false + node4 + } +} + +setup { + wait until primary, secondary timeout 120s + wait until node3 state is secondary + and node4 state is secondary + timeout 60s + promote node1 +} + +teardown { + compose down +} + +# +# test_004: set the whole formation to async +# + +step test_004_set_async { + exec node1 pg_autoctl set formation number-sync-standbys 0 + exec node1 pg_autoctl set node replication-quorum false + exec node2 pg_autoctl set node replication-quorum false + exec node3 pg_autoctl set node replication-quorum false + wait until node1 state is primary timeout 60s +} + +# +# test_005: write into primary +# + +step test_005_write_into_primary { + sql node1 { CREATE TABLE t1(a int); } + sql node1 { INSERT INTO t1 VALUES (1), (2), (3), (4); } + sql node1 { CHECKPOINT; } +} + +# +# test_006: async failover — promote node2 +# + +step test_006_async_failover { + exec monitor pg_autoctl perform promotion --name node2 + wait until node1 state is secondary + and node3 state is secondary + and node2 state is primary + timeout 90s +} + +# +# test_007: read from new primary +# + +step test_007_read_from_new_primary { + sql node2 { SELECT * FROM t1; } + expect { { 1 } { 2 } { 3 } { 4 } } +} + +# +# test_008: set mixed sync/async configuration +# + +step test_008_set_sync_async { + exec node1 pg_autoctl set node replication-quorum true + exec node2 pg_autoctl set node replication-quorum true + exec node3 pg_autoctl set node replication-quorum false + exec node3 pg_autoctl set node candidate-priority 0 + wait until node2 state is primary timeout 60s +} + +step test_009_add_sync_standby { + exec node2 pg_autoctl set formation number-sync-standbys 1 + wait until node1 state is secondary + and node2 state is primary + and node3 state is secondary + and node4 state is secondary + timeout 60s +} + +# +# test_010: promote node1 via monitor SQL +# + +step test_010_promote_node1 { + sql monitor { + SELECT pgautofailover.perform_promotion('default', 'node1'); + } +} + +# +# test_011: ifdown node4 while it is at report_lsn +# + +step test_011_ifdown_node4_at_reportlsn { + wait until node4 state is report_lsn timeout 120s + network disconnect node4 + wait until node3 state is secondary timeout 120s +} + +# +# test_012: ifup node4 +# + +step test_012_ifup_node4 { + network connect node4 + wait until node3 state is secondary + and node4 state is secondary + and node1 state is primary + and node2 state is secondary + timeout 90s +} + +# +# test_013: drop node4 +# + +step test_013_drop_node4 { + exec monitor pg_autoctl drop node --name node4 + compose stop node4 + wait until node1 state is primary + and node2 state is secondary + and node3 state is secondary + timeout 90s +} + +step test_014_001_fail_node1 { + compose stop node1 + wait until node2 state is stop_replication timeout 120s + wait until node2 state is wait_primary + and node3 state is secondary + timeout 120s + sql node2 { SHOW synchronous_standby_names; } + expect { } +} + +step test_014_001b_write_and_checkpoint { + sql node2 { INSERT INTO t1 VALUES (100); } + sql node2 { CHECKPOINT; } + sleep 3s +} + +step test_014_002_stop_new_primary_node2 { + compose stop node2 + wait until node3 state is report_lsn timeout 90s +} + +step test_014_003_restart_node1 { + compose start node1 + wait until node1 state is wait_primary timeout 180s +} + +step test_014_004_restart_node2 { + compose start node2 + wait until node2 state is secondary + and node3 state is secondary + and node1 state is primary + timeout 300s +} + +step test_015_001_fail_primary_node1 { + compose stop node1 + wait until node2 state is wait_primary timeout 120s +} + +step test_015_001b_write_and_checkpoint { + sql node2 { INSERT INTO t1 VALUES (101); } + sql node2 { CHECKPOINT; } + sleep 3s +} + +step test_015_002_fail_new_primary_node2 { + compose stop node2 + wait until node3 state is report_lsn timeout 90s +} + +step test_015_003_restart_node2 { + compose start node2 + wait until node2 state is wait_primary + and node3 state is secondary + timeout 120s +} + +step test_015_004_restart_node1 { + compose start node1 + wait until node3 state is secondary + and node2 state is primary + and node1 state is secondary + timeout 120s +} + +step test_016_001_fail_node3 { + network disconnect node3 + wait until node3 assigned-state = catchingup timeout 90s +} + +step test_016_002_fail_node1 { + network disconnect node1 + wait until node2 state is wait_primary timeout 90s +} + +step test_016_003_restart_node3 { + network connect node3 + wait until node3 assigned-state = secondary timeout 90s + wait until node2 state is wait_primary timeout 60s + sleep 5s +} + +step test_016_004_restart_node1 { + network connect node1 + wait until node3 state is secondary + and node2 state is primary + and node1 state is secondary + timeout 90s +} + +step test_017_001_fail_node1 { + network disconnect node1 + wait until node2 state is wait_primary timeout 90s +} + +step test_017_002_fail_node3 { + network disconnect node3 + wait until node3 assigned-state = catchingup timeout 90s +} + +step test_017_003_restart_nodes { + network connect node3 + network connect node1 + wait until node3 state is secondary + and node2 state is primary + and node1 state is secondary + timeout 90s +} diff --git a/tests/tap/specs/multi_ifdown.pgaf b/tests/tap/specs/multi_ifdown.pgaf new file mode 100644 index 000000000..a3f1d0417 --- /dev/null +++ b/tests/tap/specs/multi_ifdown.pgaf @@ -0,0 +1,178 @@ +# Test network-partition (ifdown/ifup) scenarios with three nodes and +# explicit candidate-priority settings. +# Covers: basic ifdown of a standby while writes continue on the primary, +# failover when the primary is disconnected and a lagging candidate reconnects, +# and an advanced split-brain scenario where both the primary and the +# most-advanced secondary are cleanly stopped before a behind async node is +# promoted — requiring pg_rewind to fetch missing WAL from the survivors. +# +# Note: the advanced test uses compose stop (clean shutdown) rather than +# network disconnect for the primary/advanced secondary so Postgres does not +# recycle WAL segments, which would corrupt pg_rewind's prev-links. +# +# Predecessor: tests/test_multi_ifdown.py + +cluster { + monitor + ssl off + formation { + node1 + node2 + node3 + } +} + +setup { + wait until primary, secondary timeout 120s + wait until node3 state is secondary timeout 60s + promote node1 +} + +teardown { + compose down +} + +# +# test_004: write into primary +# + +step test_004_write_into_primary { + sql node1 { CREATE TABLE t1(a int); } + sql node1 { INSERT INTO t1 VALUES (1), (2), (3), (4); } + sql node1 { CHECKPOINT; } + sql node1 { SELECT count(*) FROM t1; } + expect { 4 } +} + +# +# test_005: set candidate priorities +# node1 priority=90 (primary) +# node2 priority=0 (not a candidate) +# node3 priority=90 +# + +step test_005_set_candidate_priorities { + exec node1 pg_autoctl set node candidate-priority 90 + exec node2 pg_autoctl set node candidate-priority 0 + exec node3 pg_autoctl set node candidate-priority 90 + wait until node1 state is primary + and node2 state is secondary + and node3 state is secondary + timeout 60s + sql node1 { SHOW synchronous_standby_names; } + expect { ANY 1 (pgautofailover_standby_3, pgautofailover_standby_2) } +} + +# +# test_006: take node3 offline (ifdown) +# + +step test_006_ifdown_node3 { + network disconnect node3 +} + +# +# test_007: insert rows while node3 is down; node2 is sync and gets WAL +# + +step test_007_insert_rows { + sql node1 { + INSERT INTO t1 SELECT x+10 FROM generate_series(1, 10000) as gs(x); + } + sql node1 { CHECKPOINT; } +} + +# +# test_008: fail node1 (primary), reconnect node3, verify node3 becomes primary +# + +step test_008_failover { + network disconnect node1 + network connect node3 + wait until node3 state is wait_primary timeout 120s + wait until node2 state is secondary + and node3 state is primary + timeout 120s + sql node3 { SHOW synchronous_standby_names; } + expect { ANY 1 (pgautofailover_standby_1, pgautofailover_standby_2) } +} + +# +# test_009: read from new primary +# + +step test_009_read_from_new_primary { + sql node3 { SELECT count(*) FROM t1; } + expect { 10004 } +} + +# +# test_010: start node1 again as secondary +# + +step test_010_start_node1_again { + network connect node1 + wait until node1 state is secondary + and node2 state is secondary + and node3 state is primary + timeout 90s + sql node3 { SHOW synchronous_standby_names; } + expect { ANY 1 (pgautofailover_standby_1, pgautofailover_standby_2) } +} + +# +# test_011–012: prepare for advanced scenario +# Promote node2, make node2 async so it can fall behind +# + +step test_011_prepare_candidate_priorities { + exec node2 pg_autoctl set node candidate-priority 100 +} + +step test_012_prepare_replication_quorums { + exec node3 pg_autoctl set formation number-sync-standbys 0 + exec node2 pg_autoctl set node replication-quorum false +} + +step test_013_secondary_gets_behind_primary { + network disconnect node2 + sql node3 { INSERT INTO t1 VALUES (5), (6); } + sql node3 { CHECKPOINT; } + sql node1 { SELECT count(*) FROM t1; } + expect { 10006 } + sleep 2s +} + +# +# test_014: trigger failover with node3 (primary) and node1 (most advanced) +# both offline; only node2 (behind, async candidate) is online +# + +step test_014_secondary_reports_lsn { + wait until node1 state is secondary + and node3 state is primary + timeout 60s + compose stop node3 + compose stop node1 + network connect node2 + exec monitor bash -c "pg_autoctl perform failover || true" + wait until node2 state is report_lsn timeout 90s +} + +step test_015_start_node3_node1 { + compose start node3 + compose start node1 +} + +# +# test_015: bring back node1 and node3 so node2 can fetch missing WAL +# + +step test_015_finalize_failover { + wait until node1 state is secondary + and node3 state is secondary + and node2 state is primary + timeout 300s + sql node2 { SELECT count(*) FROM t1; } + expect { 10006 } +} diff --git a/tests/tap/specs/multi_maintenance.pgaf b/tests/tap/specs/multi_maintenance.pgaf new file mode 100644 index 000000000..85a19ad69 --- /dev/null +++ b/tests/tap/specs/multi_maintenance.pgaf @@ -0,0 +1,278 @@ +# Test the maintenance mode lifecycle with four nodes (three sync + one async). +# Covers: putting a standby in maintenance while triggering failover, +# both standbys in maintenance while primary keeps running, attempting to +# stop the primary while no standby is available (must fail), enabling +# maintenance on the primary with --allow-failover, and verifying the monitor +# enforces the invariant that at least one non-maintenance standby always +# remains when number-sync-standbys > 0. +# +# node4 starts async / candidate-priority 0; test_013 promotes it to +# full quorum participant to simulate dynamic node addition. +# +# Predecessor: tests/test_multi_maintenance.py + +cluster { + monitor + ssl off + formation { + node1 + node2 + node3 + node4 candidate-priority 0 replication-quorum false + } +} + +setup { + wait until primary, secondary timeout 120s + wait until node3 state is secondary timeout 60s + promote node1 +} + +teardown { + compose down +} + +# +# test_004: write into primary +# + +step test_004_write_into_primary { + sql node1 { CREATE TABLE t1(a int); } + sql node1 { INSERT INTO t1 VALUES (1), (2), (3), (4); } + sql node1 { CHECKPOINT; } + sql node1 { SELECT count(*) FROM t1; } + expect { 4 } +} + +step test_005_set_candidate_priorities { + exec node1 pg_autoctl set node candidate-priority 80 + exec node2 pg_autoctl set node candidate-priority 70 + exec node3 pg_autoctl set node candidate-priority 90 + wait until node1 state is primary + and node2 state is secondary + and node3 state is secondary + timeout 60s + sql node1 { SHOW synchronous_standby_names; } + expect { ANY 1 (pgautofailover_standby_3, pgautofailover_standby_2) } +} + +# +# test_006a: put node2 in maintenance, failover to node3, then disable maintenance +# + +step test_006a_maintenance_and_failover { + exec node2 pg_autoctl enable maintenance + wait until node2 state is maintenance timeout 60s + stop postgres node2 + wait until node1 state is primary timeout 60s + sql node1 { SHOW synchronous_standby_names; } + expect { ANY 1 (pgautofailover_standby_3, pgautofailover_standby_2) } + exec monitor pg_autoctl perform failover + wait until node3 state is primary + and node1 state is secondary + timeout 90s + sql node3 { SHOW synchronous_standby_names; } + expect { ANY 1 (pgautofailover_standby_1, pgautofailover_standby_2) } + exec node2 pg_autoctl disable maintenance + wait until node1 state is secondary + and node2 state is secondary + and node3 state is primary + timeout 90s + sql node3 { SHOW synchronous_standby_names; } + expect { ANY 1 (pgautofailover_standby_1, pgautofailover_standby_2) } +} + +# +# test_006b: read from new primary +# + +step test_006b_read_from_new_primary { + sql node3 { SELECT * FROM t1; } + expect { { 1 } { 2 } { 3 } { 4 } } +} + +# +# test_007a: put node1 in maintenance +# + +step test_007a_node1_to_maintenance { + wait until node3 state is primary timeout 60s + exec node1 pg_autoctl enable maintenance + wait until node3 state is primary timeout 60s +} + +# +# test_007b: put node2 in maintenance; both standbys now in maintenance +# + +step test_007b_node2_to_maintenance { + exec node2 pg_autoctl enable maintenance + wait until node3 state is primary timeout 60s + sql node3 { SHOW synchronous_standby_names; } + expect { ANY 1 (pgautofailover_standby_1, pgautofailover_standby_2) } +} + +# +# test_008a: stop the primary (node3) while standbys are in maintenance +# node3 must NOT be assigned draining (both secondaries are in maintenance) +# + +step test_008a_stop_primary { + assert node3 stays primary while { + network disconnect node3 + sleep 30s + } + wait until node3 assigned-state = primary timeout 90s +} + +# +# test_008b: restart primary node3 +# + +step test_008b_start_primary { + network connect node3 + wait until node3 state is primary timeout 90s +} + +step test_009a_enable_maintenance_on_primary_should_fail { + exec-fails node3 pg_autoctl enable maintenance --allow-failover +} + +# +# test_009b: disable maintenance on node1 and node2 +# + +step test_009b_disable_maintenance { + exec node1 pg_autoctl disable maintenance + exec node2 pg_autoctl disable maintenance + wait until node1 state is secondary + and node2 state is secondary + and node3 state is primary + timeout 90s +} + +step test_010_set_number_sync_standby_to_zero { + exec node3 pg_autoctl set formation number-sync-standbys 0 +} + +# +# test_011: put both standbys in maintenance; primary becomes wait_primary +# + +step test_011_all_to_maintenance { + wait until node3 state is primary timeout 60s + exec node1 pg_autoctl enable maintenance + wait until node3 state is primary timeout 60s + exec node2 pg_autoctl enable maintenance + wait until node3 state is wait_primary timeout 90s + sql node3 { SHOW synchronous_standby_names; } + expect { } +} + +# +# test_012: can write while in wait_primary (number-sync-standbys=0) +# + +step test_012_can_write_during_maintenance { + sql node3 { INSERT INTO t1 VALUES (5), (6); } + sql node3 { CHECKPOINT; } +} + +step test_013_add_standby { + exec node4 pg_autoctl set node replication-quorum true + exec node4 pg_autoctl set node candidate-priority 50 + wait until node4 state is secondary + and node3 state is primary + and node2 state is maintenance + and node1 state is maintenance + timeout 60s +} + +# +# test_014: disable maintenance on node2 and node1 +# + +step test_014_disable_maintenance { + wait until node2 state is maintenance timeout 60s + exec node2 pg_autoctl disable maintenance + wait until node3 state is primary timeout 90s + wait until node1 state is maintenance timeout 60s + exec node1 pg_autoctl disable maintenance + wait until node3 state is primary timeout 90s +} + +step test_015_set_number_sync_standby_to_one { + exec node3 pg_autoctl set formation number-sync-standbys 1 + wait until node3 state is primary timeout 60s +} + +# +# test_016: put two standbys in maintenance (number-sync-standbys=1, primary stays primary) +# + +step test_016_two_standbys_in_maintenance { + exec node1 pg_autoctl enable maintenance + wait until node3 state is primary timeout 60s + exec node2 pg_autoctl enable maintenance + wait until node3 state is primary timeout 60s +} + +step test_017_primary_to_maintenance { + exec-fails node3 pg_autoctl enable maintenance +} + +# +# test_018: disable maintenance on node2 and node1 +# + +step test_018_disable_maintenance { + wait until node2 state is maintenance timeout 60s + exec node2 pg_autoctl disable maintenance + wait until node3 state is primary timeout 90s + wait until node1 state is maintenance timeout 60s + exec node1 pg_autoctl disable maintenance + wait until node3 state is primary timeout 90s +} + +# +# test_019: set priorities so node1 is the preferred candidate +# + +step test_019_set_priorities { + exec node1 pg_autoctl set node candidate-priority 90 + exec node2 pg_autoctl set node candidate-priority 70 + exec node3 pg_autoctl set node candidate-priority 70 + exec node4 pg_autoctl set node candidate-priority 70 + wait until node1 state is secondary + and node2 state is secondary + and node3 state is primary + and node4 state is secondary + timeout 60s +} + +# +# test_020: put primary (node3) in maintenance with failover allowed +# + +step test_020_primary_to_maintenance { + wait until node3 state is primary timeout 60s + exec node3 pg_autoctl enable maintenance --allow-failover + wait until node3 state is maintenance + and node2 state is secondary + and node4 state is secondary + and node1 state is primary + timeout 90s +} + +# +# test_021: disable maintenance on node3, it rejoins as secondary +# + +step test_021_stop_maintenance { + exec node3 pg_autoctl disable maintenance + wait until node3 state is secondary + and node1 state is primary + and node2 state is secondary + and node4 state is secondary + timeout 90s +} diff --git a/tests/tap/specs/multi_standbys.pgaf b/tests/tap/specs/multi_standbys.pgaf new file mode 100644 index 000000000..7f55aa14d --- /dev/null +++ b/tests/tap/specs/multi_standbys.pgaf @@ -0,0 +1,303 @@ +# Test multi-standby formation features with four nodes. +# Covers: candidate-priority and replication-quorum APIs (get/set/validate), +# number_sync_standbys get/set and the auto-decrement trigger when a node is +# dropped, failover and read-from-all-nodes verification, network-partition +# failover, unblocking writes by setting number-sync-standbys=0 while two +# standbys are down, and the edge case where all candidate priorities are zero +# (no promotion possible until one priority is raised to ≥1). +# +# Predecessor: tests/test_multi_standbys.py + +cluster { + monitor + ssl off + formation { + node1 + node2 + node3 + node4 + } +} + +setup { + wait until primary, secondary timeout 120s + wait until node2 state is secondary + and node3 state is secondary + and node4 state is secondary + timeout 60s + promote node1 +} + +teardown { + compose down +} + +# +# test_002: candidate priority API +# + +step test_002_candidate_priority { + exec node1 pg_autoctl get node candidate-priority + expect { 50 } + exec-fails node1 pg_autoctl set node candidate-priority -1 + exec node1 pg_autoctl get node candidate-priority + expect { 50 } + exec node1 pg_autoctl set node candidate-priority 99 + exec node1 pg_autoctl get node candidate-priority + expect { 99 } +} + +# +# test_003: replication quorum API +# + +step test_003_replication_quorum { + exec node1 pg_autoctl get node replication-quorum + expect { true } + exec-fails node1 pg_autoctl set node replication-quorum "wrong quorum" + exec node1 pg_autoctl get node replication-quorum + expect { true } + exec node1 pg_autoctl set node replication-quorum false + exec node1 pg_autoctl get node replication-quorum + expect { false } + exec node1 pg_autoctl set node replication-quorum true + exec node1 pg_autoctl get node replication-quorum + expect { true } +} + +step test_004_001_add_three_standbys { + wait until node2 state is secondary + and node1 state is primary + timeout 60s +} + +step test_004_002_add_three_standbys { + wait until node3 state is secondary + and node1 state is primary + timeout 60s +} + +step test_004_003_add_three_standbys { + wait until node4 state is secondary + and node1 state is primary + timeout 60s +} + +# +# test_005: number_sync_standbys +# + +step test_005_number_sync_standbys { + exec node1 pg_autoctl set formation number-sync-standbys 2 + sql node1 { SHOW synchronous_standby_names; } + expect { ANY 2 (pgautofailover_standby_2, pgautofailover_standby_3, pgautofailover_standby_4) } + exec node1 pg_autoctl set formation number-sync-standbys 0 + sql node1 { SHOW synchronous_standby_names; } + expect { ANY 1 (pgautofailover_standby_2, pgautofailover_standby_3, pgautofailover_standby_4) } + exec node1 pg_autoctl set formation number-sync-standbys 1 + sql node1 { SHOW synchronous_standby_names; } + expect { ANY 1 (pgautofailover_standby_2, pgautofailover_standby_3, pgautofailover_standby_4) } +} + +step test_006_number_sync_standbys_trigger { + exec node1 pg_autoctl set formation number-sync-standbys 2 + exec monitor pg_autoctl drop node --name node4 + compose stop node4 + wait until node1 state is primary timeout 90s + sql node1 { SHOW synchronous_standby_names; } + expect { ANY 1 (pgautofailover_standby_2, pgautofailover_standby_3) } + sleep 6s +} + +# +# test_007: create table t1 +# + +step test_007_create_t1 { + sql node1 { CREATE TABLE t1(a int); } + sql node1 { INSERT INTO t1 VALUES (1), (2); } + sql node1 { CHECKPOINT; } +} + +step test_008_set_candidate_priorities { + exec node1 pg_autoctl set node candidate-priority 90 + exec node2 pg_autoctl set node candidate-priority 90 + exec node3 pg_autoctl set node candidate-priority 70 + wait until node1 state is primary timeout 60s +} + +# +# test_009: failover — node2 becomes primary +# + +step test_009_failover { + exec monitor pg_autoctl perform failover + wait until node2 state is primary + and node3 state is secondary + and node1 state is secondary + timeout 90s + sql node2 { SHOW synchronous_standby_names; } + expect { ANY 1 (pgautofailover_standby_1, pgautofailover_standby_3) } +} + +# +# test_010: read from all nodes +# + +step test_010_read_from_nodes { + sql node1 { SELECT * FROM t1; } + expect { { 1 } { 2 } } + sql node2 { SELECT * FROM t1; } + expect { { 1 } { 2 } } + sql node3 { SELECT * FROM t1; } + expect { { 1 } { 2 } } +} + +# +# test_011: write into new primary +# + +step test_011_write_into_new_primary { + sql node2 { INSERT INTO t1 VALUES (3), (4); } + sql node2 { CHECKPOINT; } +} + +# +# test_012: fail primary node2 +# + +step test_012_fail_primary { + network disconnect node2 + wait until node1 state is stop_replication timeout 90s + wait until node1 state is primary + and node3 state is secondary + timeout 90s + sql node1 { SHOW synchronous_standby_names; } + expect { ANY 1 (pgautofailover_standby_2, pgautofailover_standby_3) } +} + +# +# test_013: restart node2 as secondary +# + +step test_013_restart_node2 { + network connect node2 + wait until node1 state is primary + and node2 state is secondary + and node3 state is secondary + timeout 90s + sql node1 { SHOW synchronous_standby_names; } + expect { ANY 1 (pgautofailover_standby_2, pgautofailover_standby_3) } +} + +step test_014_001_fail_set_properties { + exec node1 pg_autoctl set node candidate-priority 50 + exec node2 pg_autoctl set node candidate-priority 50 + exec node3 pg_autoctl set node candidate-priority 50 + wait until node1 state is primary timeout 60s +} + +step test_014_002_fail_two_standby_nodes { + compose stop node2 + compose stop node3 + wait until node2 assigned-state = catchingup timeout 180s + wait until node3 assigned-state = catchingup timeout 180s + # node1 remains primary (blocking writes at postgres level) until + # number-sync-standbys is explicitly set to 0 in the next step. + wait until node1 state is primary timeout 180s + sql node1 { SHOW synchronous_standby_names; } + expect { ANY 1 (pgautofailover_standby_2, pgautofailover_standby_3) } +} + +step test_014_003_unblock_writes { + exec node1 pg_autoctl set formation number-sync-standbys 0 + wait until node1 state is wait_primary timeout 90s + sql node1 { SHOW synchronous_standby_names; } + expect { } +} + +step test_014_004_restart_nodes { + compose start node3 + compose start node2 + wait until node3 state is secondary + and node2 state is secondary + and node1 state is primary + timeout 90s + sql node1 { SHOW synchronous_standby_names; } + expect { ANY 1 (pgautofailover_standby_2, pgautofailover_standby_3) } +} + +step test_015_002_fail_two_standby_nodes { + network disconnect node2 + network disconnect node3 + wait until node1 state is wait_primary timeout 90s + sql node1 { SHOW synchronous_standby_names; } + expect { } +} + +step test_015_003_set_properties { + sql monitor { + SELECT pgautofailover.set_formation_number_sync_standbys('default', 1); + } + wait until node1 assigned-state = primary timeout 90s + sql node1 { SHOW synchronous_standby_names; } + expect { ANY 1 (pgautofailover_standby_2, pgautofailover_standby_3) } +} + +step test_015_004_restart_nodes { + network connect node3 + network connect node2 + wait until node3 state is secondary + and node2 state is secondary + and node1 state is primary + timeout 90s + sql node1 { SHOW synchronous_standby_names; } + expect { ANY 1 (pgautofailover_standby_2, pgautofailover_standby_3) } +} + +step test_016_001_set_candidate_priorities_to_zero { + exec node1 pg_autoctl set node candidate-priority 0 + exec node2 pg_autoctl set node candidate-priority 0 + exec node3 pg_autoctl set node candidate-priority 0 + wait until node1 state is primary timeout 60s +} + +step test_016_002_trigger_failover { + exec monitor bash -c "pg_autoctl perform failover --wait 1 || true" + wait until node3 state is report_lsn + and node2 state is report_lsn + and node1 state is report_lsn + timeout 90s +} + +step test_016_003_set_candidate_priority_to_one { + exec node2 pg_autoctl set node candidate-priority 1 + wait until node2 state is primary + and node1 state is secondary + and node3 state is secondary + timeout 90s +} + +step test_016_004_reset_candidate_priority { + exec node2 pg_autoctl set node candidate-priority 0 + wait until node2 state is primary + and node1 state is secondary + and node3 state is secondary + timeout 60s +} + +step test_016_005_perform_promotion { + exec monitor pg_autoctl perform promotion --name node1 + wait until node1 state is primary + and node2 state is secondary + and node3 state is secondary + timeout 90s +} + +step test_017_remove_old_primary { + exec monitor pg_autoctl drop node --name node2 + compose stop node2 + wait until node1 state is primary + and node3 state is secondary + timeout 90s +} diff --git a/tests/tap/specs/nonha_citus_operation.pgaf b/tests/tap/specs/nonha_citus_operation.pgaf new file mode 100644 index 000000000..e607f025e --- /dev/null +++ b/tests/tap/specs/nonha_citus_operation.pgaf @@ -0,0 +1,104 @@ +# Test non-HA Citus formation lifecycle: start with secondary=false, add +# workers, enable secondary support, add standbys, attempt to disable while +# standbys exist (expect failure), fail-over primaries, drop them, and finally +# disable secondary support once only single nodes remain. +# +# Ported from tests/test_nonha_citus_operation.py +# Predecessor: tests/test_nonha_citus_operation.py + +cluster { + monitor + formation non-ha { + coordinator1a coordinator + coordinator1b coordinator launch deferred + worker1a worker group 1 + worker1b worker group 1 launch deferred + worker2a worker group 2 + worker2b worker group 2 launch deferred + } +} + +setup { + wait until coordinator1a state is single timeout 120s + wait until worker1a state is single timeout 120s + wait until worker2a state is single timeout 120s + sleep 5s +} + +teardown { + compose down +} + +step test_001_disable_secondary_on_formation { + exec monitor pg_autoctl disable secondary --formation non-ha +} + +step test_002_wait_metadata_sync { + exec coordinator1a sh -c 'for i in $(seq 1 30); do n=$(psql -U docker -d demo -tAc "SELECT count(*) FROM pg_dist_node WHERE metadatasynced = false AND isactive = true"); [ "$n" = "0" ] && exit 0; sleep 2; done; exit 1' +} + +step test_003_create_distributed_table { + sql coordinator1a { CREATE TABLE t1 (a int); } + sql coordinator1a { SELECT create_distributed_table('t1', 'a'); } + sql coordinator1a { INSERT INTO t1 VALUES (1), (2); } +} + +step test_004_enable_secondary { + exec monitor pg_autoctl enable secondary --formation non-ha + exec coordinator1b pg_autoctl node start /etc/pgaf/node.ini + exec worker1b pg_autoctl node start /etc/pgaf/node.ini + exec worker2b pg_autoctl node start /etc/pgaf/node.ini +} + +step test_005_add_secondaries { + wait until coordinator1b state is secondary timeout 120s + wait until worker1b state is secondary timeout 120s + wait until worker2b state is secondary timeout 120s +} + +step test_006_fail_when_disabling_with_secondaries { + exec-fails monitor pg_autoctl disable secondary --formation non-ha +} + +step test_007_failover_coordinator { + network disconnect coordinator1a + wait until coordinator1b state is wait_primary timeout 90s + network connect coordinator1a + wait until coordinator1b state is primary + and coordinator1a state is secondary + timeout 180s +} + +step test_007b_failover_worker1 { + network disconnect worker1a + wait until worker1b state is wait_primary timeout 90s + network connect worker1a + wait until worker1b state is primary + and worker1a state is secondary + timeout 180s +} + +step test_007c_failover_worker2 { + network disconnect worker2a + wait until worker2b state is wait_primary timeout 90s + network connect worker2a + wait until worker2b state is primary + and worker2a state is secondary + timeout 180s +} + +step test_008_remove_old_primaries { + exec monitor pg_autoctl drop node --name coordinator1a --formation non-ha + exec monitor pg_autoctl drop node --name worker1a --formation non-ha + exec monitor pg_autoctl drop node --name worker2a --formation non-ha + compose stop coordinator1a + compose stop worker1a + compose stop worker2a + wait until coordinator1b state is single timeout 60s + wait until worker1b state is single timeout 60s + wait until worker2b state is single timeout 60s +} + +step test_009_disable_secondaries { + exec monitor pg_autoctl disable secondary --formation non-ha +} diff --git a/tests/tap/specs/replace_monitor.pgaf b/tests/tap/specs/replace_monitor.pgaf new file mode 100644 index 000000000..94cef2dd2 --- /dev/null +++ b/tests/tap/specs/replace_monitor.pgaf @@ -0,0 +1,95 @@ +# Test replacing a failed monitor with a new one. +# Sequence: bring up a cluster, drop the old monitor, disable it on both +# nodes, create a new monitor, re-enable it on both nodes, verify the cluster +# re-converges and a failover through the new monitor works. + +cluster { + monitor + monitor newmonitor initially stopped + ssl off + formation { + node1 + node2 + } +} + +setup { + wait until node1 state is primary timeout 90s + wait until node2 state is secondary timeout 90s +} + +teardown { + compose down +} + +step create_table { + sql node1 { CREATE TABLE t1(a int); } + sql node1 { INSERT INTO t1 VALUES (1), (2); } +} + +step read_from_secondary { + sql node2 { SELECT * FROM t1 ORDER BY a; } + expect { 1 } + expect { 2 } +} + +step drop_old_monitor { + exec monitor pg_autoctl stop +} + +step disable_monitor_on_nodes { + exec node2 pg_autoctl disable monitor --force + exec node1 pg_autoctl disable monitor --force +} + +step write_while_monitor_down { + sql node1 { INSERT INTO t1 VALUES (3); } +} + +step read_replicated_write { + sql node2 { SELECT * FROM t1 ORDER BY a; } + expect { 1 } + expect { 2 } + expect { 3 } +} + +step create_new_monitor { + compose start newmonitor + exec newmonitor pg_autoctl inspect pgsetup wait + set monitor newmonitor +} + +step enable_monitor_node1 { + exec node1 pg_autoctl enable monitor postgresql://autoctl_node@newmonitor/pg_auto_failover + wait until node1 state is single timeout 90s +} + +step enable_monitor_node2 { + exec node2 pg_autoctl enable monitor postgresql://autoctl_node@newmonitor/pg_auto_failover + wait until node2 state is catchingup timeout 90s + wait until node1 state is wait_primary timeout 90s +} + +step wait_for_convergence { + wait until node1 state is primary timeout 90s + wait until node2 state is secondary timeout 90s + wait until node1 state is primary timeout 90s + wait until node2 state is secondary timeout 90s +} + +step failover_via_new_monitor { + exec newmonitor pg_autoctl perform failover + wait until node2 state is primary timeout 90s + wait until node1 state is secondary timeout 90s + wait until node2 state is primary timeout 90s + wait until node1 state is secondary timeout 90s + sql node2 { SHOW synchronous_standby_names; } + expect { ANY 1 (pgautofailover_standby_1) } +} + +step read_from_new_secondary { + sql node1 { SELECT * FROM t1 ORDER BY a; } + expect { 1 } + expect { 2 } + expect { 3 } +} diff --git a/tests/tap/specs/skip_pg_hba.pgaf b/tests/tap/specs/skip_pg_hba.pgaf new file mode 100644 index 000000000..ef14c7f2b --- /dev/null +++ b/tests/tap/specs/skip_pg_hba.pgaf @@ -0,0 +1,58 @@ +# Test --skip-pg-hba (auth-method=skip): pg_autoctl must NOT edit pg_hba.conf +# on either node. HBA rules are managed manually by the operator. +# +# Ported from tests/test_skip_pg_hba.py +# Predecessor: tests/test_skip_pg_hba.py + +cluster { + monitor + formation { + node1 auth skip + node2 auth skip + } +} + +setup { + # With auth=skip, nodes don't add HBA rules automatically. + # Add them now so the monitor can health-check the data nodes. + exec node1 pg_autoctl inspect pgsetup hba-lan + exec node2 pg_autoctl inspect pgsetup hba-lan + wait until primary, secondary timeout 120s + promote node1 +} + +teardown { + compose down +} + +step test_002_create_t1 { + sql node1 { CREATE TABLE t1(a int); } + sql node1 { INSERT INTO t1 VALUES (1), (2); } +} + +step test_004a_hba_have_not_been_edited { + exec node1 pg_autoctl config get postgresql.hba_level + expect { skip } + exec node2 pg_autoctl config get postgresql.hba_level + expect { skip } +} + +step test_005_failover { + exec monitor pg_autoctl perform failover + wait until node2 state is primary + and node1 state is secondary + timeout 90s +} + +step test_006_restart_secondary { + compose stop node1 + compose start node1 + wait until node1 state is secondary timeout 60s +} + +step test_006a_hba_have_not_been_edited { + exec node1 pg_autoctl config get postgresql.hba_level + expect { skip } + exec node2 pg_autoctl config get postgresql.hba_level + expect { skip } +} diff --git a/tests/tap/specs/ssl_cert.pgaf b/tests/tap/specs/ssl_cert.pgaf new file mode 100644 index 000000000..aab711be7 --- /dev/null +++ b/tests/tap/specs/ssl_cert.pgaf @@ -0,0 +1,85 @@ +# Test pg_auto_failover with SSL using CA-signed certificates and cert auth. +# +# Monitor and all data nodes are set up with server certificates signed by a +# shared root CA, and client certificates are placed in ~/.postgresql/. +# Authentication uses the "cert" method: pg_autoctl writes hostssl + cert +# rules to pg_hba.conf and the ident map to pg_ident.conf automatically. +# +# Ported from tests/test_ssl_cert.py +# Predecessor: tests/test_ssl_cert.py + +cluster { + monitor + ssl verify-ca + auth cert + formation { + node1 + node2 + } +} + +setup { + wait until node1 state is primary + and node2 state is secondary + timeout 120s + promote node1 +} + +teardown { + compose down +} + +# +# test_000: verify monitor has SSL enabled with cert auth +# + +step test_000_create_monitor { + exec monitor pg_autoctl inspect pgsetup wait + sql monitor { SHOW ssl; } + expect { on } + exec monitor pg_autoctl config get ssl.sslmode + expect { verify-ca } +} + +# +# test_001: verify primary has SSL enabled with cert auth +# + +step test_001_init_primary { + wait until node1 state is primary timeout 60s + sql node1 { SHOW ssl; } + expect { on } + exec node1 pg_autoctl config get ssl.sslmode + expect { verify-ca } +} + +# +# test_002: create table t1 +# + +step test_002_create_t1 { + sql node1 { CREATE TABLE t1(a int); } + sql node1 { INSERT INTO t1 VALUES (1), (2); } +} + +# +# test_003: verify secondary has SSL enabled with cert auth +# + +step test_003_init_secondary { + wait until node2 state is secondary + and node1 state is primary + timeout 90s + sql node2 { SHOW ssl; } + expect { on } + exec node2 pg_autoctl config get ssl.sslmode + expect { verify-ca } +} + +# +# test_004: failover +# + +step test_004_failover { + exec monitor pg_autoctl perform failover +} diff --git a/tests/tap/specs/ssl_self_signed.pgaf b/tests/tap/specs/ssl_self_signed.pgaf new file mode 100644 index 000000000..c0a9f4f66 --- /dev/null +++ b/tests/tap/specs/ssl_self_signed.pgaf @@ -0,0 +1,76 @@ +# Test pg_auto_failover with self-signed SSL certificates (ssl-mode = require). +# Both the monitor and data nodes generate their own self-signed certs. +# +# Ported from tests/test_ssl_self_signed.py +# Predecessor: tests/test_ssl_self_signed.py + +cluster { + monitor + formation { + node1 + node2 + } +} + +setup { + wait until node1 state is single timeout 60s +} + +teardown { + compose down +} + +# +# test_000: create monitor with self-signed SSL +# + +step test_000_create_monitor { + exec monitor pg_autoctl inspect pgsetup wait + sql monitor { SHOW ssl; } + expect { on } + exec monitor pg_autoctl config get ssl.sslmode +} + +# +# test_001: init primary with self-signed SSL +# + +step test_001_init_primary { + wait until node1 state is single timeout 60s + sql node1 { SHOW ssl; } + expect { on } + exec node1 pg_autoctl config get ssl.sslmode +} + +# +# test_002: create table t1 +# + +step test_002_create_t1 { + sql node1 { CREATE TABLE t1(a int); } + sql node1 { INSERT INTO t1 VALUES (1), (2); } +} + +# +# test_003: add secondary node2 with self-signed SSL +# + +step test_003_init_secondary { + wait until node2 state is secondary + and node1 state is primary + timeout 90s + sql node2 { SHOW ssl; } + expect { on } + exec node2 pg_autoctl config get ssl.sslmode +} + +# +# test_004: failover +# + +step test_004_failover { + exec monitor pg_autoctl perform failover + wait until node2 state is primary + and node1 state is secondary + timeout 90s +} diff --git a/tests/tap/specs/tablespaces.pgaf b/tests/tap/specs/tablespaces.pgaf new file mode 100644 index 000000000..abcc9a85b --- /dev/null +++ b/tests/tap/specs/tablespaces.pgaf @@ -0,0 +1,148 @@ +# Tablespace HA: verify that tablespaces stored on extra volumes outside +# PGDATA survive failover, network partitions, and pg_rewind. +# +# Three tablespace directories are used across the test: +# /extra_volumes/extended_a — created before node2 joins +# /extra_volumes/extended_b — created while streaming replication is active +# /extra_volumes/extended_c — created during a network partition (wait_primary) +# +# Each node gets its own named Docker volume for each tablespace path. +# Replication copies data via WAL; volumes are not shared between nodes. +# +# Predecessor: tests/tablespaces/test_tablespaces_0[1-6].py + +cluster { + monitor + formation { + node node1 { + volume extended_a "/extra_volumes/extended_a" + volume extended_b "/extra_volumes/extended_b" + volume extended_c "/extra_volumes/extended_c" + } + node node2 { + volume extended_a "/extra_volumes/extended_a" + volume extended_b "/extra_volumes/extended_b" + volume extended_c "/extra_volumes/extended_c" + } + } +} + +setup { + wait until node1 state is primary + and node2 state is secondary + timeout 90s + exec node1 sudo chown docker /extra_volumes/extended_a /extra_volumes/extended_b /extra_volumes/extended_c + exec node2 sudo chown docker /extra_volumes/extended_a /extra_volumes/extended_b /extra_volumes/extended_c +} + +teardown { + compose down +} + +# +# test_001: create table t1 in default tablespace, then create tablespace +# extended_a and table t2 in it. node2 has not joined yet. +# + +step test_001_create_tablespace_a { + sql node1 { CREATE TABLE t1(a int); } + sql node1 { INSERT INTO t1 VALUES (1), (2); } + sql node1 { + CREATE TABLESPACE extended_a LOCATION '/extra_volumes/extended_a'; + } + sql node1 { CREATE TABLE t2(i int) TABLESPACE extended_a; } + sql node1 { INSERT INTO t2 VALUES (3), (4); } +} + +# +# test_002: node2 joins as secondary; verify that both tables — including +# t2 in extended_a — are accessible on the secondary. +# + +step test_002_init_secondary { + wait until node2 state is secondary + and node1 state is primary + timeout 90s + sql node2 { SELECT * FROM t1 ORDER BY a; } + expect { { 1 } { 2 } } + sql node2 { SELECT * FROM t2 ORDER BY i; } + expect { { 3 } { 4 } } +} + +# +# test_003: create tablespace extended_b while streaming replication is +# active; verify the new tablespace and table t3 replicate correctly. +# + +step test_003_tablespace_b_while_streaming { + sql node1 { + CREATE TABLESPACE extended_b LOCATION '/extra_volumes/extended_b'; + } + sql node1 { CREATE TABLE t3(i int) TABLESPACE extended_b; } + sql node1 { INSERT INTO t3 VALUES (5), (6); } + sql node2 { SELECT * FROM t3 ORDER BY i; } + expect { { 5 } { 6 } } +} + +# +# test_004: failover — node2 becomes primary. Write to tablespace tables +# on the new primary, verify the new secondary (node1) reads them back. +# + +step test_004_failover { + exec monitor pg_autoctl perform failover + sql node2 { INSERT INTO t2 VALUES (7); } + sql node1 { SELECT * FROM t2 ORDER BY i; } + expect { { 3 } { 4 } { 7 } } + sql node2 { INSERT INTO t3 VALUES (8); } + sql node1 { SELECT * FROM t3 ORDER BY i; } + expect { { 5 } { 6 } { 8 } } +} + +# +# test_005: network partition — disconnect node1 (now secondary). +# While node2 is in wait_primary, create tablespace extended_c and write +# to all three tablespace tables. +# + +step test_005_network_partition { + network disconnect node1 + wait until node2 state is wait_primary timeout 90s + sql node2 { + CREATE TABLESPACE extended_c LOCATION '/extra_volumes/extended_c'; + } + sql node2 { CREATE TABLE t4(i int) TABLESPACE extended_c; } + sql node2 { INSERT INTO t4 VALUES (10), (11); } + sql node2 { INSERT INTO t2 VALUES (12); } + sql node2 { INSERT INTO t3 VALUES (13); } +} + +# +# test_006: reconnect node1. pg_rewind copies WAL from node2, which +# includes the extended_c tablespace directory. After convergence node1 +# must see all writes that happened during the partition. +# + +step test_006_node1_rejoins { + network connect node1 + wait until node1 state is secondary + and node2 state is primary + timeout 90s + sql node1 { SELECT * FROM t4 ORDER BY i; } + expect { { 10 } { 11 } } + sql node1 { SELECT * FROM t2 ORDER BY i; } + expect { { 3 } { 4 } { 7 } { 12 } } + sql node1 { SELECT * FROM t3 ORDER BY i; } + expect { { 5 } { 6 } { 8 } { 13 } } +} + +# +# test_007: promote the original primary (node1) back via perform-promotion. +# + +step test_007_promote_original_primary { + exec monitor pg_autoctl perform promotion --name node1 + wait until node1 state is primary + and node2 state is secondary + timeout 90s +} diff --git a/tests/tap/specs/upgrade.pgaf b/tests/tap/specs/upgrade.pgaf new file mode 100644 index 000000000..358dfdd96 --- /dev/null +++ b/tests/tap/specs/upgrade.pgaf @@ -0,0 +1,189 @@ +# Upgrade test: live binary + extension swap without container restarts. +# +# Tests the documented production upgrade procedure: +# +# 1. Install new binary on ALL nodes while the cluster is running. +# The keeper's FSM loop fires exit(EXIT_CODE_MONITOR) only when the +# monitor extension version matches the NEW binary's required version, +# so pre-staging the binary is safe and does not trigger self-restart yet. +# +# 2. Install new binary + new extension files on the monitor, then restart +# just the monitor listener child (not the container). The listener +# detects the installed extension version ("2.1") differs from what the +# new binary requires ("2.2"), runs ALTER EXTENSION pgautofailover +# UPDATE TO '2.2', and restarts Postgres — all without stopping the +# supervisor (PID 1). +# +# 3. Each keeper's node-active child polls the monitor, sees the extension +# is now "2.2", compares against the on-disk binary's required version +# ("2.2"), finds a match, and calls exit(EXIT_CODE_MONITOR). The +# supervisor re-execs the node-active child from the new on-disk binary. +# Postgres is never restarted; only the pg_autoctl child re-execs. +# +# Image build requirements — run before this spec: +# make -C tests/upgrade pgaf-current → pgaf:current (built from v2.1 tag) +# make -C tests/upgrade pgaf-next → pgaf:next (built from current branch) +# +# pgaf:current contains BOTH binaries (2.1 and 2.2) and the v2.2 extension +# files pre-staged at /usr/local/bin/pgaf/2.2/. The "inject" steps below +# flip an in-container symlink and copy those pre-staged files — no runtime +# dependency on pgaf:next once the images are built. +# +# PGVERSION defaults to 16 (the latest PG that v2.1 supports). Rebuild with +# PGVERSION=17 make -C tests/upgrade pgaf-current pgaf-next +# if PREV_TAG supports PG17+ (and update the /16/ paths in test_003 below). + +cluster { + monitor + image "pgaf:current" + formation { + node1 + node2 + node3 + } +} + +setup { + wait until node1 state is primary + and node2 state is secondary + and node3 state is secondary + timeout 120s +} + +teardown { + compose down +} + +# ------------------------------------------------------------------------- +# Baseline: cluster running on pgaf:current (v2.1), extension at "2.1". +# Write a marker row that must survive through the upgrade. +# ------------------------------------------------------------------------- + +step test_001_baseline { + sql node1 { CREATE TABLE upgrade_marker(ts timestamptz default now()); } + sql node1 { INSERT INTO upgrade_marker DEFAULT VALUES; } + sql monitor { + SELECT installed_version + FROM pg_available_extensions + WHERE name = 'pgautofailover'; + } + expect { 2.1 } +} + +# ------------------------------------------------------------------------- +# Phase 1: flip the "current" symlink on data nodes to point at the v2.2 +# binary. Both binaries are already baked into pgaf:current — no docker cp +# is needed. The static shim at /usr/local/bin/pg_autoctl delegates to +# pgaf/current/pg_autoctl, so after the flip: +# - The keeper's on-disk version check (argv0 version --json) returns "2.2" +# - The v2.1 supervisor's next child spawn also uses v2.2 (PG_AUTOCTL_DEBUG_BIN_PATH) +# No self-restart fires yet: the monitor extension is still "2.1" and the +# keeper only exits when installed "2.2" matches on-disk required "2.2". +# ------------------------------------------------------------------------- + +step test_002_inject_node_binaries { + exec node1 ln -sfn /usr/local/bin/pgaf/2.2 /usr/local/bin/pgaf/current + exec node2 ln -sfn /usr/local/bin/pgaf/2.2 /usr/local/bin/pgaf/current + exec node3 ln -sfn /usr/local/bin/pgaf/2.2 /usr/local/bin/pgaf/current +} + +# ------------------------------------------------------------------------- +# Phase 2: flip the symlink on the monitor and install the v2.2 extension +# files (also pre-baked into pgaf:current at /usr/local/bin/pgaf/2.2/). +# Then restart just the listener child; the v2.2 listener runs: +# ALTER EXTENSION pgautofailover UPDATE TO '2.2' +# ------------------------------------------------------------------------- + +step test_003_upgrade_monitor { + # Install v2.2 extension files into system paths. The script is baked into + # the image with PGVERSION resolved at build time (no hardcoded /16/ here). + exec monitor sudo /usr/local/bin/pgaf/2.2/install-extension + # Run ALTER EXTENSION via psql BEFORE flipping the symlink so the v2.2 + # listener that starts next already sees installed_version = "2.2" and + # skips the Postgres-restart code path (which races with the supervisor). + exec monitor psql -U docker -d pg_auto_failover -c "ALTER EXTENSION pgautofailover UPDATE TO '2.2'" + # Flip the monitor symlink; from here pg_autoctl = v2.2. + exec monitor ln -sfn /usr/local/bin/pgaf/2.2 /usr/local/bin/pgaf/current + # Restart postgres on the monitor so it loads the v2.2 pgautofailover.so. + # Without this, the old .so is still resident in memory even though the + # control file and SQL catalog were updated; every keeper call to the + # monitor functions fails with "loaded library requires 2.1 but control + # file specifies 2.2", exhausting the supervisor restart limit. + exec monitor pg_autoctl manual service restart postgres --pgdata /var/lib/postgres/pgaf +} + +# ------------------------------------------------------------------------- +# Phase 3: wait for keepers to self-restart. +# +# After the monitor extension becomes "2.2", the keeper children compare +# installed "2.2" against the on-disk binary's required "2.2" — they match, +# so each child calls exit(EXIT_CODE_MONITOR) and the supervisor re-execs +# from the new binary. No Postgres restart, no container restart. +# ------------------------------------------------------------------------- + +step test_003b_wait_keeper_restart { + # Give keepers time to detect the extension upgrade and self-restart. + # v2.1 keeper polls every ~5 s; the listener restart + ALTER EXTENSION adds + # another 1-2 s. 45 s gives ample time on slow CI runners. + exec monitor sleep 45 +} + +step test_004_wait_convergence { + wait until node1 state is primary + and node2 state is secondary + and node3 state is secondary + timeout 180s +} + +# ------------------------------------------------------------------------- +# Verify the upgrade mechanism actually fired on each node. +# ------------------------------------------------------------------------- + +step test_005_verify_node_restart { + logs node1 contains "exiting for a restart of the node-active process" + logs node2 contains "exiting for a restart of the node-active process" + logs node3 contains "exiting for a restart of the node-active process" +} + +step test_006_verify_extension_version { + sql monitor { + SELECT installed_version + FROM pg_available_extensions + WHERE name = 'pgautofailover'; + } + expect { 2.2 } +} + +# ------------------------------------------------------------------------- +# Data integrity: row written before the upgrade must be readable on all +# nodes, confirming Postgres was never stopped on the data nodes. +# ------------------------------------------------------------------------- + +step test_007_verify_data_intact { + sql node1 { SELECT count(*) FROM upgrade_marker; } + expect { 1 } + sql node2 { SELECT count(*) FROM upgrade_marker; } + expect { 1 } + sql node3 { SELECT count(*) FROM upgrade_marker; } + expect { 1 } +} + +# ------------------------------------------------------------------------- +# Smoke: failover through the fully-upgraded cluster. +# ------------------------------------------------------------------------- + +step test_008_failover_post_upgrade { + exec monitor pg_autoctl perform promotion --name node2 + wait until node2 state is primary + and node1 state is secondary + and node3 state is secondary + timeout 300s +} + +step test_009_write_on_new_primary { + sql node2 { INSERT INTO upgrade_marker DEFAULT VALUES; } + sql node1 { SELECT count(*) FROM upgrade_marker; } + expect { 2 } + sql node3 { SELECT count(*) FROM upgrade_marker; } + expect { 2 } +} diff --git a/tests/upgrade/Dockerfile.current b/tests/upgrade/Dockerfile.current new file mode 100644 index 000000000..0600e2f39 --- /dev/null +++ b/tests/upgrade/Dockerfile.current @@ -0,0 +1,81 @@ +# pgaf:current — v2.1 with both binaries baked in and a static shim. +# +# Layout: +# /usr/local/bin/pg_autoctl — static bash shim (never replaced) +# /usr/local/bin/pgaf/2.1/pg_autoctl — v2.1 binary +# /usr/local/bin/pgaf/2.2/pg_autoctl — v2.2 binary (from pgaf:next) +# /usr/local/bin/pgaf/current — symlink → pgaf/2.1 (initially) +# +# "Injecting" an upgrade = ln -sf /usr/local/bin/pgaf/2.2 /usr/local/bin/pgaf/current +# Run that inside each container via `docker compose exec`; no docker cp needed. +# +# Why this works for the self-restart mechanism: +# +# The shim always lives at /usr/local/bin/pg_autoctl. It delegates every +# command to pgaf/current/pg_autoctl via `exec -a /usr/local/bin/pg_autoctl` +# so the real binary sees argv[0] = /usr/local/bin/pg_autoctl. That path is +# what pg_autoctl saves as pg_autoctl_argv0, and later uses for the on-disk +# version check: +# +# run_program(pg_autoctl_argv0, "version", "--json") +# +# After the symlink flip, that invocation runs the shim → delegates to v2.2 → +# returns required_extension_version "2.2". Combined with the monitor +# extension just upgraded to "2.2", the keeper detects a match and calls +# exit(EXIT_CODE_MONITOR) so the supervisor re-execs from the new binary. +# +# PG_AUTOCTL_DEBUG_BIN_PATH=/usr/local/bin/pg_autoctl is set in ENV so the +# v2.1 supervisor's pg_autoctl_program also resolves to the shim path, and +# child processes spawned after the symlink flip pick up v2.2. +# +# Startup command translation: +# +# pgaftest generates: pg_autoctl node run /etc/pgaf/node.ini (v2.2 syntax) +# v2.1 does not know "node run"; the shim translates it per node kind. + +ARG BASE_IMAGE=pgaf:current-base +ARG NEXT_IMAGE=pgaf:next + +FROM ${NEXT_IMAGE} AS next +FROM ${BASE_IMAGE} + +USER root + +# bash is required for "exec -a name" (sets argv[0] for the real binary). +RUN apt-get update \ + && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends bash \ + && rm -rf /var/lib/apt/lists/* + +# Place both binaries side by side; current → 2.1 initially. +RUN mkdir -p /usr/local/bin/pgaf/2.1 /usr/local/bin/pgaf/2.2 +RUN mv /usr/local/bin/pg_autoctl /usr/local/bin/pgaf/2.1/pg_autoctl +COPY --from=next /usr/local/bin/pg_autoctl /usr/local/bin/pgaf/2.2/pg_autoctl +RUN ln -s /usr/local/bin/pgaf/2.1 /usr/local/bin/pgaf/current \ + && chown -R docker /usr/local/bin/pgaf + +# Pre-stage v2.2 extension files so the upgrade step is a local install, not +# a docker cp from an external image. Stored alongside the v2.2 binary. +ARG PGVERSION=16 +COPY --from=next /usr/share/postgresql/${PGVERSION}/extension/pgautofailover.control \ + /usr/local/bin/pgaf/2.2/pgautofailover.control +COPY --from=next /usr/share/postgresql/${PGVERSION}/extension/pgautofailover--2.1--2.2.sql \ + /usr/local/bin/pgaf/2.2/pgautofailover--2.1--2.2.sql +COPY --from=next /usr/share/postgresql/${PGVERSION}/extension/pgautofailover--2.2.sql \ + /usr/local/bin/pgaf/2.2/pgautofailover--2.2.sql +COPY --from=next /usr/lib/postgresql/${PGVERSION}/lib/pgautofailover.so \ + /usr/local/bin/pgaf/2.2/pgautofailover.so + +# Install script — uses pg_config at runtime so PGVERSION need not be baked in. +# Usage (inside the container, as root): /usr/local/bin/pgaf/2.2/install-extension +COPY install-extension.sh /usr/local/bin/pgaf/2.2/install-extension +RUN chmod 755 /usr/local/bin/pgaf/2.2/install-extension + +# Static shim — stays at the canonical path forever. +COPY pg_autoctl_shim.sh /usr/local/bin/pg_autoctl +RUN chmod 755 /usr/local/bin/pg_autoctl + +# Tell the v2.1 supervisor to use the shim path for spawning child processes, +# so that after the symlink flip the next child spawn picks up v2.2. +ENV PG_AUTOCTL_DEBUG_BIN_PATH=/usr/local/bin/pg_autoctl + +USER docker diff --git a/tests/upgrade/Makefile b/tests/upgrade/Makefile index 92f17b6da..e178b2022 100644 --- a/tests/upgrade/Makefile +++ b/tests/upgrade/Makefile @@ -1,56 +1,104 @@ -NODES ?= 3 - -PATCH = tests/upgrade/monitor-upgrade-1.7.patch -Q_VERSION = select default_version, installed_version -Q_VERSION += from pg_available_extensions where name = 'pgautofailover' - -build: - docker compose build - -patch: - cd ../.. && git apply $(PATCH) - +# tests/upgrade/Makefile +# +# Builds the two Docker images needed for the upgrade spec and runs it. +# +# Usage: +# make build both images and run the upgrade spec +# make pgaf-current build pgaf:current from PREV_TAG +# make pgaf-next build pgaf:next from current working tree +# make run run tests/tap/specs/upgrade.pgaf +# make clean remove the two images +# +# Customisation: +# PREV_TAG — git tag to build pgaf:current from (default: auto-detected) +# PGVERSION — Postgres major version baked into the image (default: 16) +# +# PGVERSION defaults to 16 because v2.1 (the current PREV_TAG) only supports +# up to PG16 — its monitor extension explicitly rejects PG >= 17. Override +# with PGVERSION=17 only when testing against a PREV_TAG that supports PG17+. +# +# Auto-detection of PREV_TAG: reads PG_AUTOCTL_EXTENSION_VERSION from +# defaults.h to get the current extension version (e.g. "2.2"), then +# finds the most recent tag whose version is strictly lower. The tag +# matching assumes the form "vMAJOR.MINOR". +# +# Examples: +# make PREV_TAG=v2.0 # test 2.0→2.2 on default PG16 +# make PREV_TAG=v2.1 PGVERSION=16 # explicit PG16 + +PGVERSION ?= 16 +PGAFTEST ?= pgaftest +SPEC := $(realpath $(dir $(abspath $(lastword $(MAKEFILE_LIST))))/../tap/specs/upgrade.pgaf) +REPO_ROOT := $(realpath $(dir $(abspath $(lastword $(MAKEFILE_LIST))))/../..) +DOCKERFILE := $(REPO_ROOT)/Dockerfile + +# Current extension version from source +CURRENT_EXT := $(shell grep 'PG_AUTOCTL_EXTENSION_VERSION ' \ + $(REPO_ROOT)/src/bin/pg_autoctl/defaults.h \ + | grep -o '"[^"]*"' | tr -d '"') + +# Previous release tag: the highest vX.Y tag whose X.Y < CURRENT_EXT. +# Works for two-component versions (2.1, 2.2, …). +# Override with PREV_TAG=v2.0 if the heuristic gives the wrong answer. +_PREV_TAG_DETECT := $(shell \ + cur=$(CURRENT_EXT); \ + git -C $(REPO_ROOT) tag --sort=-version:refname \ + | grep -E '^v[0-9]+[.][0-9]+$$' \ + | grep -v "^v$$cur$$" \ + | head -1) +PREV_TAG ?= $(_PREV_TAG_DETECT) + +.PHONY: all pgaf-current pgaf-next run clean + +all: pgaf-next pgaf-current run + +## Build pgaf:current — the previous release, used as the starting point. +## Built from the git archive of PREV_TAG so that uncommitted changes in +## the working tree do not contaminate the "old" image. +## +## git archive produces a clean tree without git history, so git-version.h +## (a generated file) is absent. We extract the archive into a tempdir, +## synthesise git-version.h from the tag name, then docker build the dir. +pgaf-current: + @if [ -z "$(PREV_TAG)" ]; then \ + echo "ERROR: could not auto-detect PREV_TAG; set it explicitly:"; \ + echo " make PREV_TAG=v2.1"; \ + exit 1; \ + fi + @echo "Building pgaf:current from $(PREV_TAG) (extension version from that tag)" + $(eval _TMP := $(shell mktemp -d)) + git -C $(REPO_ROOT) archive $(PREV_TAG) | tar -x -C $(_TMP) + printf '#define GIT_VERSION "%s"\n' '$(PREV_TAG)' \ + > $(_TMP)/src/bin/pg_autoctl/git-version.h + cp $(REPO_ROOT)/Dockerfile $(_TMP)/Dockerfile + docker build \ + --build-arg PGVERSION=$(PGVERSION) \ + --target run \ + -t pgaf:current-base \ + $(_TMP) + rm -rf $(_TMP) + docker build \ + --build-arg BASE_IMAGE=pgaf:current-base \ + --build-arg NEXT_IMAGE=pgaf:next \ + -f $(dir $(abspath $(lastword $(MAKEFILE_LIST))))Dockerfile.current \ + -t pgaf:current \ + $(dir $(abspath $(lastword $(MAKEFILE_LIST)))) + +## Build pgaf:next — the current working tree (the new version being tested). +pgaf-next: + @echo "Building pgaf:next from working tree (extension version $(CURRENT_EXT))" + docker build \ + --build-arg PGVERSION=$(PGVERSION) \ + --target run \ + -t pgaf:next \ + $(REPO_ROOT) + +## Run the upgrade spec. +## Requires pgaf:current and pgaf:next to already be built. +run: + @echo "Running upgrade spec: $(SPEC)" + $(PGAFTEST) run $(SPEC) + +## Remove the two upgrade test images. clean: - cd ../.. && git apply --reverse $(PATCH) - -up: create-volumes compose-up tail ; - -down: compose-down rm-volumes ; - -compose-down: - docker compose down --volumes --remove-orphans - -compose-up: - docker compose up -d - -tail: - docker compose logs -f - -create-volumes: - for v in volm vol1 vol2 vol3; do docker volume create $$v; done - -rm-volumes: - for v in volm vol1 vol2 vol3; do docker volume rm $$v; done - -upgrade-monitor: patch - docker compose up -d --no-deps --build monitor - -upgrade-nodes: - docker compose up -d --no-deps --build node3 node2 - docker compose up -d --no-deps --build node1 - -state: - docker compose exec monitor pg_autoctl show state - -version: - docker compose exec monitor pg_autoctl version - docker compose exec monitor psql -d pg_auto_failover -c "$(Q_VERSION)" - -failover: - docker compose exec monitor pg_autoctl perform failover - -watch: - docker compose exec monitor watch -n 0.2 pg_autoctl show state - -.PHONY: build patch clean up down upgrade-monitor state watch -.PHONY: compose-down compose-up create-volumes rm-volumes + -docker rmi pgaf:current pgaf:next 2>/dev/null || true diff --git a/tests/upgrade/install-extension.sh b/tests/upgrade/install-extension.sh new file mode 100644 index 000000000..c9392d58d --- /dev/null +++ b/tests/upgrade/install-extension.sh @@ -0,0 +1,14 @@ +#!/bin/sh +# Install the pre-staged v2.2 pgautofailover extension files into the system +# extension and library paths reported by pg_config. Run as root inside the +# pgaf:current container. +set -e + +STAGING=/usr/local/bin/pgaf/2.2 +EXTDIR=$(pg_config --sharedir)/extension +LIBDIR=$(pg_config --pkglibdir) + +install -m 644 "${STAGING}/pgautofailover.control" "${EXTDIR}/" +install -m 644 "${STAGING}/pgautofailover--2.1--2.2.sql" "${EXTDIR}/" +install -m 644 "${STAGING}/pgautofailover--2.2.sql" "${EXTDIR}/" +install -m 755 "${STAGING}/pgautofailover.so" "${LIBDIR}/" diff --git a/tests/upgrade/pg_autoctl_shim.sh b/tests/upgrade/pg_autoctl_shim.sh new file mode 100644 index 000000000..a659ce2fc --- /dev/null +++ b/tests/upgrade/pg_autoctl_shim.sh @@ -0,0 +1,95 @@ +#!/bin/bash +# Static shim at /usr/local/bin/pg_autoctl — never replaced during the upgrade. +# +# Delegates to /usr/local/bin/pgaf/current/pg_autoctl, which is a symlink: +# initially → pgaf/2.1 (v2.1 binary) +# after upgrade → pgaf/2.2 (v2.2 binary) +# +# exec -a sets argv[0] to /usr/local/bin/pg_autoctl so the real binary saves +# that path as pg_autoctl_argv0. The keeper uses that path for the on-disk +# version check (pg_autoctl_argv0 version --json). After the symlink flip +# that invocation hits this shim again → delegates to v2.2 → returns "2.2". +# +# PG_AUTOCTL_DEBUG_BIN_PATH is set in ENV (Dockerfile.current) so the v2.1 +# supervisor's pg_autoctl_program is also /usr/local/bin/pg_autoctl; child +# processes after the symlink flip therefore pick up v2.2. +# +# Startup translation: +# pgaftest generates: pg_autoctl node run /etc/pgaf/node.ini (v2.2 syntax) +# v2.1 does not know "node run"; we translate per the [node] kind in the ini. + +SHIM=/usr/local/bin/pg_autoctl +CURRENT=/usr/local/bin/pgaf/current/pg_autoctl + +ini_get() { + awk -F'[[:space:]]*=[[:space:]]*' \ + -v sec="$1" -v key="$2" \ + '/^\[/{in_sec=($0 == "["sec"]")} in_sec && $1==key{print $2; exit}' "$3" +} + +if [ "$1" = "node" ] && [ "$2" = "run" ] && [ -n "$3" ]; then + ini="$3" + + kind=$(ini_get node kind "$ini") + hostname=$(ini_get node hostname "$ini") + port=$(ini_get node port "$ini") + pgdata=$(ini_get postgresql pgdata "$ini") + monitor=$(ini_get monitor pguri "$ini") + name=$(ini_get node name "$ini") + formation=$(ini_get formation name "$ini") + ssl=$(ini_get options ssl "$ini") + auth=$(ini_get options auth "$ini") + + [ -z "$pgdata" ] && { echo "pg_autoctl_shim: missing [postgresql] pgdata in $ini" >&2; exit 1; } + + case "$ssl" in + off|"") ssl_flag="--no-ssl" ;; + *) ssl_flag="--ssl-self-signed" ;; + esac + + set -- \ + ${hostname:+--hostname "$hostname"} \ + ${port:+--pgport "$port"} \ + ${auth:+--auth "$auth"} \ + "$ssl_flag" + + cfg_dir=$(printf '%s' "$pgdata" | sed 's|^/||') + cfg="/var/lib/postgres/.config/pg_autoctl/${cfg_dir}/pg_autoctl.cfg" + + if [ "$kind" = "monitor" ]; then + if [ ! -f "$cfg" ]; then + # v2.1 create monitor supports --run (stays in the foreground). + exec -a "$SHIM" "$CURRENT" create monitor --pgdata "$pgdata" "$@" --run + else + exec -a "$SHIM" "$CURRENT" run --pgdata "$pgdata" + fi + else + set -- "$@" \ + ${monitor:+--monitor "$monitor"} \ + ${name:+--name "$name"} \ + ${formation:+--formation "$formation"} + + if [ ! -f "$cfg" ]; then + # v2.1 create postgres does NOT have --run; exec the run loop after. + "$CURRENT" create postgres --pgdata "$pgdata" "$@" + rc=$?; [ $rc -ne 0 ] && exit $rc + fi + + exec -a "$SHIM" "$CURRENT" run --pgdata "$pgdata" + fi +fi + +# v2.1 supervisor spawns its children as "do service {node-active,postgres,listener}". +# v2.2 renamed the "do" subcommand tree to "internal". After the symlink flip +# ($CURRENT resolves to pgaf/2.2) the v2.1 supervisor's child-spawn calls still +# use "do service X" — translate to "internal service X" so v2.2 accepts them. +# Before the flip $CURRENT → pgaf/2.1 and the v2.1 binary already understands "do". +if [ "$1" = "do" ] && [ "$2" = "service" ]; then + resolved=$(readlink /usr/local/bin/pgaf/current 2>/dev/null) + if [ "$resolved" = "/usr/local/bin/pgaf/2.2" ]; then + exec -a "$SHIM" "$CURRENT" "internal" "${@:2}" + fi +fi + +# All other commands (version --json, run --pgdata respawn, …) +exec -a "$SHIM" "$CURRENT" "$@" From b0917c5516671f7c1db3a9d56511f673eaa33681 Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Tue, 7 Jul 2026 00:26:58 +0200 Subject: [PATCH 14/68] chore: untrack pgaftest binary, add to .gitignore --- .gitignore | 1 + src/bin/pgaftest/pgaftest | Bin 870520 -> 0 bytes 2 files changed, 1 insertion(+) delete mode 100755 src/bin/pgaftest/pgaftest diff --git a/.gitignore b/.gitignore index 94b523b1c..de6657523 100644 --- a/.gitignore +++ b/.gitignore @@ -55,3 +55,4 @@ valgrind/ *.tmp src/bin/pgaftest/test_spec_parse.tab.* src/bin/pgaftest/test_spec_parse.output +src/bin/pgaftest/pgaftest diff --git a/src/bin/pgaftest/pgaftest b/src/bin/pgaftest/pgaftest deleted file mode 100755 index f71a9013a5b9dd44c3bbeb4feafaced8c578e819..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 870520 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

XdTlq%Bwnm@%Gq1F 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

=F{o$W8OeUg)e%f{(BD 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 From a648f0a89f9e30e783239e9ce792125afa84b0b1 Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Tue, 7 Jul 2026 00:29:59 +0200 Subject: [PATCH 15/68] ci: expand pgaftest workflow to full PG14-17 matrix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Split the single build job into: - build-images: matrix over PG14/15/16/17, one pgaf:run-pgNN artifact each - build-pgaftest: single job (PG17), binary is version-agnostic at runtime The test matrix now crosses every spec × every PG version (19 specs × 4 versions = 76 parallel jobs), matching the run-tests.yml coverage scope. installcheck likewise runs on all four versions. This positions pgaftest as the replacement for run-tests.yml once all scenarios are ported — run-tests.yml will be removed at that point. --- .github/workflows/run-pgaftest.yml | 128 ++++++++++++++++------------- 1 file changed, 70 insertions(+), 58 deletions(-) diff --git a/.github/workflows/run-pgaftest.yml b/.github/workflows/run-pgaftest.yml index 0f0be634d..c8b6c0300 100644 --- a/.github/workflows/run-pgaftest.yml +++ b/.github/workflows/run-pgaftest.yml @@ -16,46 +16,61 @@ concurrency: jobs: # --------------------------------------------------------------------------- - # Build the runtime Docker image and the pgaftest binary once, then share - # both as artifacts with every parallel test job. + # Build one pgaf:run image per Postgres version and upload each as an + # artifact. All downstream test jobs download only the version they need. # --------------------------------------------------------------------------- - build: - name: Build image & pgaftest + build-images: + name: Build image (PG${{ matrix.PGVERSION }}) runs-on: ubuntu-latest - env: - PGVERSION: 17 + strategy: + fail-fast: false + matrix: + PGVERSION: [14, 15, 16, 17] steps: - uses: actions/checkout@v4.2.2 - # git-version.h is generated from the working tree and must exist on the - # host before the Docker build COPY step reads it. - name: Generate git-version.h run: make version - - name: Build pg_auto_failover run image + - name: Build pgaf:run image (PG${{ matrix.PGVERSION }}) run: | docker build \ - --build-arg PGVERSION=${{ env.PGVERSION }} \ + --build-arg PGVERSION=${{ matrix.PGVERSION }} \ --target run \ - -t pgaf:run \ + -t pgaf:run-pg${{ matrix.PGVERSION }} \ . - - name: Save pgaf:run image as a tarball - run: docker save pgaf:run | gzip > /tmp/pgaf-run.tar.gz + - name: Save image to tarball + run: | + docker save pgaf:run-pg${{ matrix.PGVERSION }} \ + | gzip > /tmp/pgaf-run-pg${{ matrix.PGVERSION }}.tar.gz - - name: Upload pgaf:run image artifact + - name: Upload image artifact uses: actions/upload-artifact@v4.4.3 with: - name: pgaf-run-image - path: /tmp/pgaf-run.tar.gz - # Only needed for the duration of this workflow run. + name: pgaf-run-image-pg${{ matrix.PGVERSION }} + path: /tmp/pgaf-run-pg${{ matrix.PGVERSION }}.tar.gz retention-days: 1 - # Build the full build image just to extract the pgaftest binary. - # The binary is linux/amd64, linked against PGDG libpq — matches - # ubuntu-latest runners where the test jobs will execute it. - - name: Build pg_auto_failover build image (for pgaftest extraction) + # --------------------------------------------------------------------------- + # Build the pgaftest binary once from the PG17 build image. + # The binary links against libpq at runtime, so it works with any PG version + # as long as the matching libpq5 package is installed on the runner. + # --------------------------------------------------------------------------- + build-pgaftest: + name: Build pgaftest binary + runs-on: ubuntu-latest + env: + PGVERSION: 17 + + steps: + - uses: actions/checkout@v4.2.2 + + - name: Generate git-version.h + run: make version + + - name: Build pg_auto_failover build image run: | docker build \ --build-arg PGVERSION=${{ env.PGVERSION }} \ @@ -63,7 +78,7 @@ jobs: -t pgaf:build \ . - - name: Extract pgaftest binary from build image + - name: Extract pgaftest binary run: | docker create --name pgaftest-extract pgaf:build docker cp pgaftest-extract:/usr/lib/postgresql/${{ env.PGVERSION }}/bin/pgaftest \ @@ -71,7 +86,7 @@ jobs: docker rm pgaftest-extract chmod +x /tmp/pgaftest-bin - - name: Upload pgaftest binary artifact + - name: Upload pgaftest binary uses: actions/upload-artifact@v4.4.3 with: name: pgaftest-binary @@ -79,17 +94,18 @@ jobs: retention-days: 1 # --------------------------------------------------------------------------- - # Run all non-upgrade specs from tests/tap/schedule in a matrix. - # Each job is independent; they all share the pre-built image and binary. + # Run every spec against every supported Postgres version. + # Each job downloads its matching pgaf:run-pgNN image and the shared binary. # --------------------------------------------------------------------------- test: - name: pgaftest / ${{ matrix.spec }} - needs: build + name: pgaftest / ${{ matrix.spec }} (PG${{ matrix.PGVERSION }}) + needs: [build-images, build-pgaftest] runs-on: ubuntu-latest timeout-minutes: 25 strategy: fail-fast: false matrix: + PGVERSION: [14, 15, 16, 17] spec: - basic_operation - basic_operation_listen_flag @@ -114,14 +130,17 @@ jobs: steps: - uses: actions/checkout@v4.2.2 - - name: Download pgaf:run image + - name: Download pgaf:run image (PG${{ matrix.PGVERSION }}) uses: actions/download-artifact@v4.1.8 with: - name: pgaf-run-image + name: pgaf-run-image-pg${{ matrix.PGVERSION }} path: /tmp - name: Load pgaf:run image into Docker - run: docker load < /tmp/pgaf-run.tar.gz + run: | + docker load < /tmp/pgaf-run-pg${{ matrix.PGVERSION }}.tar.gz + # Alias to the name pgaftest expects via PGAF_IMAGE + docker tag pgaf:run-pg${{ matrix.PGVERSION }} pgaf:run - name: Download pgaftest binary uses: actions/download-artifact@v4.1.8 @@ -129,8 +148,7 @@ jobs: name: pgaftest-binary path: /tmp - # pgaftest links against PGDG libpq — install the matching runtime lib. - - name: Install libpq runtime + - name: Install libpq runtime (PG${{ matrix.PGVERSION }}) run: | sudo apt-get install -y curl ca-certificates gnupg curl https://www.postgresql.org/media/keys/ACCC4CF8.asc \ @@ -150,17 +168,19 @@ jobs: /tmp/pgaftest-bin run tests/tap/specs/${{ matrix.spec }}.pgaf # --------------------------------------------------------------------------- - # installcheck: builds pgaf:testrun (run + source tree + postgresql-server-dev) - # and runs the SQL regression test suite via pg_regress. Kept separate from - # the main matrix because it requires a larger image built inline. + # installcheck: SQL regression suite via pg_regress. + # Runs on all supported Postgres versions; builds pgaf:testrun inline since + # it needs postgresql-server-dev which isn't in the run image. # --------------------------------------------------------------------------- installcheck: - name: pgaftest / installcheck - needs: build + name: pgaftest / installcheck (PG${{ matrix.PGVERSION }}) + needs: build-pgaftest runs-on: ubuntu-latest timeout-minutes: 20 - env: - PGVERSION: 17 + strategy: + fail-fast: false + matrix: + PGVERSION: [14, 15, 16, 17] steps: - uses: actions/checkout@v4.2.2 @@ -185,10 +205,10 @@ jobs: - name: Generate git-version.h run: make version - - name: Build pgaf:testrun image + - name: Build pgaf:testrun image (PG${{ matrix.PGVERSION }}) run: | docker build \ - --build-arg PGVERSION=${{ env.PGVERSION }} \ + --build-arg PGVERSION=${{ matrix.PGVERSION }} \ --target testrun \ -t pgaf:testrun \ . @@ -200,14 +220,13 @@ jobs: /tmp/pgaftest-bin run tests/tap/specs/installcheck.pgaf # --------------------------------------------------------------------------- - # Upgrade test: builds both pgaf:next (current branch) and pgaf:current - # (the previous release tag, auto-detected), then runs the upgrade spec. - # Kept separate because it needs a PGVERSION=16 image and additional - # build steps (pgaf:current-base from git archive of PREV_TAG). + # Upgrade test: binary + extension swap without container restart. + # Pinned to PG16 (the upgrade path always runs N-1 → current). + # Builds its own pgaf:next and pgaf:current images from the upgrade Makefile. # --------------------------------------------------------------------------- upgrade: name: pgaftest / upgrade - needs: build + needs: build-pgaftest runs-on: ubuntu-latest timeout-minutes: 30 env: @@ -216,8 +235,7 @@ jobs: steps: - uses: actions/checkout@v4.2.2 with: - # Full history needed so PREV_TAG auto-detection (git tag --sort) - # finds the correct previous release tag. + # Full history needed so PREV_TAG auto-detection finds the right tag. fetch-depth: 0 - name: Install libpq runtime @@ -240,17 +258,11 @@ jobs: - name: Generate git-version.h run: make version - # The build job already produced pgaf:run at PG16 for the regular tests, - # but the upgrade job runs independently and needs its own images. - # Build pgaf:next first so pgaf:current's Dockerfile.current can COPY - # the v2.2 binary and extension files from it. - - name: Build pgaf:next (current branch, PGVERSION=16) - run: | - make -C tests/upgrade pgaf-next PGVERSION=${{ env.PGVERSION }} + - name: Build pgaf:next (current branch, PG${{ env.PGVERSION }}) + run: make -C tests/upgrade pgaf-next PGVERSION=${{ env.PGVERSION }} - - name: Build pgaf:current (previous release + both binaries baked in) - run: | - make -C tests/upgrade pgaf-current PGVERSION=${{ env.PGVERSION }} + - name: Build pgaf:current (previous release, PG${{ env.PGVERSION }}) + run: make -C tests/upgrade pgaf-current PGVERSION=${{ env.PGVERSION }} - name: Run upgrade spec timeout-minutes: 25 From 04abed7038ee6e5a76aaa0ae6c85e7b457b12cd7 Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Tue, 7 Jul 2026 00:40:14 +0200 Subject: [PATCH 16/68] feat: pgaftest setup --tmux opens interactive 3-pane session MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three panes, laid out evenly: top — docker compose logs -f (all services) middle — pg_autoctl watch (on the monitor) bottom — bash inside the first non-deferred data node Previously the session was created detached (-d) and the user had to manually run 'tmux attach'. Now runner_setup attaches immediately after creating the session so the terminal lands inside tmux. The shell pane target is the first non-deferred data node in the spec's formation block; falls back to the monitor when no data node is found. --- src/bin/pgaftest/test_runner.c | 50 ++++++++++++++++++++++++++++------ 1 file changed, 42 insertions(+), 8 deletions(-) diff --git a/src/bin/pgaftest/test_runner.c b/src/bin/pgaftest/test_runner.c index 355c3aea5..3459894ee 100644 --- a/src/bin/pgaftest/test_runner.c +++ b/src/bin/pgaftest/test_runner.c @@ -3582,22 +3582,56 @@ runner_setup(TestSpec *spec, const char *workDir, bool withTmux) if (withTmux) { - /* Launch tmux with compose logs + pg_autoctl watch */ - log_info("Starting tmux session for project %s", r.projectName); + /* + * Pick the first non-deferred data node as the interactive shell + * target. Falls back to the monitor if no data node is found. + */ + const char *shellNode = r.activeMonitorService; + + for (int fi = 0; fi < spec->cluster.formationCount; fi++) + { + const TestFormation *form = &spec->cluster.formations[fi]; + + for (int ni = 0; ni < form->nodeCount; ni++) + { + const TestNode *n = &form->nodes[ni]; + + if (!n->launchDeferred) + { + shellNode = n->name; + fi = spec->cluster.formationCount; /* break outer loop */ + break; + } + } + } + + /* + * Build a three-pane tmux session: + * top — docker compose logs -f + * middle — pg_autoctl watch on the monitor + * bottom — interactive bash in the first data node + * + * Created detached first, then immediately attached so the current + * terminal lands inside the session. + */ + log_info("Starting tmux session \"%s\" (shell target: %s)", + r.projectName, shellNode); + run_cmd( "tmux new-session -d -s %s " "\"%s logs -f\" \\; " "split-window -v " - "\"%s exec monitor pg_autoctl watch\" \\; " - "split-window -v \\; " + "\"%s exec %s pg_autoctl watch\" \\; " + "split-window -v " + "\"%s exec -it %s bash\" \\; " "select-layout even-vertical", r.projectName, r.composeBase, - r.composeBase); + r.composeBase, r.activeMonitorService, + r.composeBase, shellNode); - printf("Cluster ready. Attach with: tmux attach -t %s\n", - r.projectName); - printf("Tear down with: pgaftest down --work-dir %s\n", workDir); + /* Attach the current terminal into the session. */ + run_cmd("tmux attach-session -t %s", r.projectName); } else { From 7c0303bf1ae59b7b42517845d6e7aaee0f177e1c Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Tue, 7 Jul 2026 00:47:12 +0200 Subject: [PATCH 17/68] fix: parse options that follow the spec file path (pgaftest setup spec --tmux) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The commandline library calls getopt, which on POSIX/macOS stops scanning at the first non-option argument (the spec file path). Any flags given after the spec file — the natural invocation form for setup/run -- are silently ignored, so --tmux and --work-dir have no effect. Fix: after cli_setup extracts argv[0] as the spec path, reset getopt state (optreset=1 on BSD, optind=1 everywhere) and call pgaftest_getopts again on the remaining argv. argv[0] serves as a dummy program name so getopt starts scanning from index 1, picking up --tmux, --work-dir, etc. Also applies to the tmux session creation: - session is now attached immediately (tmux attach-session after new-session -d) - third pane execs bash inside the first non-deferred data node --- src/bin/pgaftest/cli_root.c | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/bin/pgaftest/cli_root.c b/src/bin/pgaftest/cli_root.c index facb8837e..177f1a806 100644 --- a/src/bin/pgaftest/cli_root.c +++ b/src/bin/pgaftest/cli_root.c @@ -310,6 +310,19 @@ cli_setup(int argc, char **argv) strncpy(pgaftestOpts.specFile, argv[0], sizeof(pgaftestOpts.specFile) - 1); + /* + * The commandline library stops getopt at the first non-option (the spec + * file path), so flags that follow it — e.g. `pgaftest setup spec --tmux` + * — are not seen on the first pass. Re-run getopts now: argv[0] acts as + * a dummy program name so getopt starts scanning from index 1 onward, + * picking up --tmux, --work-dir, etc. optreset resets BSD/macOS state. + */ +#ifdef __BSD_VISIBLE + optreset = 1; +#endif + optind = 1; + pgaftest_getopts(argc, argv); + if (pgaftestOpts.workDir[0] == '\0') { derive_work_dir(pgaftestOpts.specFile, From ef2becaaa41540d774f3feed71411f4a991f141a Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Tue, 7 Jul 2026 01:10:35 +0200 Subject: [PATCH 18/68] pgaftest: fix setup --tmux timing, fix down --work-dir, add docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit setup --tmux timing The setup{} block now runs inside the tmux bottom pane instead of before tmux is created. The user immediately gets the three-pane session; the bottom pane shows setup progress and transitions to an interactive bash shell when done. - add runner_run_setup_only() — runs setup{} against an existing stack - add pgaftest _setup_ sub-command — internal entry-point for the pane - runner_setup() launches tmux first (after compose up + monitor ready) then the bottom pane calls _setup_ and exec's bash on success - set pg_autoctl_program to argv[0] in main() so runner_setup can reference the binary path in the tmux pane command pgaftest down --work-dir When no spec file is found, cli_down now derives the project name from the workDir basename and runs: docker compose -p -f /docker-compose.yml down ... instead of a bare docker compose down that silently targets nothing. docs - docs/ref/pgaftest.rst: full reference (sub-commands, DSL, env vars, TAP) - docs/operations.rst: new Testing section - docs/tutorial.rst: pgaftest alternative section with interactive_tutorial.pgaf - docs/citus-quickstart.rst: pgaftest alternative with citus_tutorial.pgaf - docs/index.rst: add ref/pgaftest to toctree - docs/tutorial/interactive_tutorial.pgaf: two-node HA interactive spec - docs/tutorial/citus_tutorial.pgaf: eight-node Citus interactive spec --- docs/ref/pgaftest.rst | 394 ++++++++++++++++++++++++ docs/tutorial/citus_tutorial.pgaf | 57 ++++ docs/tutorial/interactive_tutorial.pgaf | 48 +++ 3 files changed, 499 insertions(+) create mode 100644 docs/ref/pgaftest.rst create mode 100644 docs/tutorial/citus_tutorial.pgaf create mode 100644 docs/tutorial/interactive_tutorial.pgaf diff --git a/docs/ref/pgaftest.rst b/docs/ref/pgaftest.rst new file mode 100644 index 000000000..6c543cc65 --- /dev/null +++ b/docs/ref/pgaftest.rst @@ -0,0 +1,394 @@ +.. _pgaftest: + +pgaftest +======== + +``pgaftest`` is the pg_auto_failover integration test runner. It reads +``.pgaf`` spec files that describe a cluster topology and a set of test steps, +spins up the cluster using Docker Compose, and drives the steps to completion. + +Two modes of operation are supported: + +- **CI mode** (``pgaftest run``): headless TAP output, non-zero exit on failure. +- **Interactive mode** (``pgaftest setup``): cluster stays up, shell or tmux + session opened for hands-on exploration. + +.. contents:: + :local: + :depth: 2 + + +Synopsis +-------- + +:: + + pgaftest run [options] + pgaftest run --schedule [options] + pgaftest setup [options] [--tmux] + pgaftest step [--work-dir

] + pgaftest show + pgaftest prepare [] + pgaftest down [--work-dir ] + + +Sub-commands +------------ + +.. _pgaftest_run: + +``pgaftest run`` +~~~~~~~~~~~~~~~~ + +Run a spec in CI mode. Docker Compose is started, the ``setup {}`` block +runs, each step in ``sequence`` runs in order, ``teardown {}`` always runs, +and the stack is removed. Output is TAP: ``ok N - step_name`` / ``not ok``. + +:: + + pgaftest run tests/tap/specs/basic_operation.pgaf + +Options: + +``--schedule `` + Run every spec listed in the file (one name or path per line). + +``--expected `` + Compare TAP output against ``.out`` files in the directory. + +``--work-dir `` + Working directory for compose files and state (default: + ``$TMPDIR/pgaftest/``). + +``--no-cleanup`` + Leave the compose stack running after the run for post-mortem inspection. + Use ``pgaftest down`` to clean up manually. + +.. _pgaftest_setup: + +``pgaftest setup`` +~~~~~~~~~~~~~~~~~~ + +Start a cluster interactively. Docker Compose is started and the +``setup {}`` block runs. Control is then handed back to you. + +:: + + pgaftest setup tests/tap/specs/basic_operation.pgaf + pgaftest setup docs/tutorial/interactive_tutorial.pgaf --tmux + +Options: + +``--tmux`` + Open a three-pane tmux session immediately after the cluster is ready. + The setup block runs in the bottom pane so you can watch the logs pane + while the cluster initialises. The three panes are: + + - **top** — ``docker compose logs -f`` (live container output) + - **middle** — ``pg_autoctl watch`` on the monitor + - **bottom** — interactive ``bash`` in the first data node (once setup + completes) + +``--work-dir `` + Working directory (default: ``$TMPDIR/pgaftest/``). + +.. _pgaftest_step: + +``pgaftest step`` +~~~~~~~~~~~~~~~~~ + +Run a single named step against a cluster that was started with +``pgaftest setup``. + +:: + + pgaftest step stop_primary --work-dir /tmp/pgaftest/basic_operation + +The step name must match a ``step {}`` block in the spec. The spec +file is read from ``/spec.pgaf``. + +.. _pgaftest_down: + +``pgaftest down`` +~~~~~~~~~~~~~~~~~ + +Run the ``teardown {}`` block (if any) and then ``docker compose down +--volumes --remove-orphans``. + +:: + + pgaftest down --work-dir /tmp/pgaftest/basic_operation + pgaftest down tests/tap/specs/basic_operation.pgaf + +When called with only ``--work-dir``, the project name is derived from the +last path component of the working directory, matching the name that +``pgaftest setup`` used when creating the stack. + +.. _pgaftest_show: + +``pgaftest show`` +~~~~~~~~~~~~~~~~~ + +Print the ``docker-compose.yml`` that would be generated from the spec, +without running anything. + +:: + + pgaftest show tests/tap/specs/basic_operation.pgaf + +.. _pgaftest_prepare: + +``pgaftest prepare`` +~~~~~~~~~~~~~~~~~~~~ + +Write ``docker-compose.yml``, per-node ``.ini`` files, and a ``Makefile`` +to an output directory for manual inspection or customisation. + +:: + + pgaftest prepare tests/tap/specs/basic_operation.pgaf ./my-cluster + + +Spec file format +---------------- + +A ``.pgaf`` spec file has four top-level sections. Only ``cluster {}`` is +required; the others are optional. + +.. code-block:: text + + # comments start with # + + cluster { + ... # topology declaration + } + + setup { + ... # commands to run after compose up + } + + teardown { + ... # commands to run on cleanup + } + + step { + ... # named test step + } + + sequence + step1 + step2 + ... + +The ``cluster {}`` block +~~~~~~~~~~~~~~~~~~~~~~~~ + +Declares the Docker Compose topology. Every cluster must have at least one +formation with at least one node. + +.. code-block:: text + + cluster { + monitor # required for multi-node HA + image "pg_auto_failover:pg17" # optional; default: build from source + + ssl self-signed # self-signed | cert | off (default: self-signed) + auth trust # trust | md5 | scram (default: trust) + + formation { # optional name; default name: "default" + node1 + node2 + node3 async candidate-priority 0 + } + + # Citus: multiple formations + formation workers num-sync 1 { + w1 worker group 1 + w2 worker group 1 + } + } + +Node modifiers: + +============================================ ============================================= +``async`` Mark as async standby (no sync quorum) +``candidate-priority `` Failover priority 0–100 (default: 50) +``launch deferred`` Container starts with ``sleep infinity``; + use ``exec node pg_autoctl node start`` +``coordinator`` / ``worker group `` Citus role +``no-monitor`` Standalone node (no monitor) +``listen`` Bind all interfaces (``--listen 0.0.0.0``) +``auth `` Per-node auth override +``ssl `` Per-node SSL override +``volume `` Mount a named Docker volume at ```` +============================================ ============================================= + +Commands inside ``setup``, ``teardown``, and ``step`` blocks +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**State waits** + +.. code-block:: text + + wait until state is [timeout s] + wait until primary, secondary [timeout s] + wait until stopped [timeout s] + +The multi-node form ``wait until primary, secondary`` waits until at least +one node in each named state is present in the formation. + +**Assertions** + +.. code-block:: text + + assert state is + assert assigned-state is + assert stays while { + # commands that must not trigger a transition + } + +**Execute commands in a container** + +.. code-block:: text + + exec # must exit 0 + exec-fails # must exit non-zero + +**SQL** + +.. code-block:: text + + sql { SELECT ... } + expect { } + expect error [] + +``expect`` matches the output of the immediately preceding ``sql`` command. +Fields are separated by whitespace; rows are wrapped in ``{ }``. + +**Network** + +.. code-block:: text + + network disconnect + network connect + +**Compose lifecycle** + +.. code-block:: text + + compose stop + compose start + compose kill + compose down + +**PostgreSQL control** + +.. code-block:: text + + stop postgres + start postgres + +**Failover** + +.. code-block:: text + + promote [, , ...] + +**Monitor targeting** (replace-monitor tests) + +.. code-block:: text + + set monitor + +**Log grep** + +.. code-block:: text + + logs [not] + +**Sleep** + +.. code-block:: text + + sleep s + + +Environment variables +--------------------- + +``PGAF_IMAGE`` + Override the Docker image used for all node containers. Useful in CI to + avoid rebuilding the image on every run:: + + PGAF_IMAGE=pgaf:run pgaftest run tests/tap/specs/basic_operation.pgaf + +``PGAFTEST_COMPOSE_SERVICE`` + Name of the service inside which ``pgaftest`` itself runs when executing + inside a container. Used by the CI workflow to locate the binary. + +``PGAFTEST_HOST_WORK_DIR`` + Host-side working directory when ``pgaftest`` runs inside a container. + Bind-mounted so compose files written by the binary are visible to the + Docker daemon on the host. + +``TMPDIR`` + Base directory for auto-derived working directories + (default: ``/tmp``). Working directories are placed under + ``$TMPDIR/pgaftest/``. + + +TAP output +---------- + +In ``run`` mode, pgaftest produces `TAP version 13`__ output on standard +output. Each named step in ``sequence`` becomes one test point:: + + TAP version 13 + 1..4 + ok 1 - stop_primary + ok 2 - check_failover + ok 3 - restart_node1 + ok 4 - verify_replication + +On failure:: + + not ok 2 - check_failover + # DIAG: wait until node2 state is primary: timed out after 90s + +__ https://testanything.org/tap-version-13-specification.html + + +Examples +-------- + +**Run a single spec**:: + + pgaftest run tests/tap/specs/basic_operation.pgaf + +**Run the full schedule**:: + + pgaftest run --schedule tests/tap/schedule + +**Bring up an interactive cluster, watch it in tmux**:: + + pgaftest setup docs/tutorial/interactive_tutorial.pgaf --tmux + +**Run a named step against a live cluster**:: + + pgaftest step stop_primary --work-dir /tmp/pgaftest/basic_operation + +**Tear down a cluster started with** ``--no-cleanup`` **or** ``setup``:: + + pgaftest down --work-dir /tmp/pgaftest/basic_operation + +**Print the generated docker-compose.yml without running anything**:: + + pgaftest show tests/tap/specs/basic_operation.pgaf + + +See also +-------- + +- :ref:`tutorial` — Docker Compose tutorial with ``pgaftest`` alternative +- :ref:`citus_quickstart` — Citus tutorial with ``pgaftest`` alternative +- :ref:`pg_autoctl_watch` diff --git a/docs/tutorial/citus_tutorial.pgaf b/docs/tutorial/citus_tutorial.pgaf new file mode 100644 index 000000000..72bf1c2a2 --- /dev/null +++ b/docs/tutorial/citus_tutorial.pgaf @@ -0,0 +1,57 @@ +# Citus cluster: one coordinator pair + three worker pairs, all with HA. +# +# Usage: +# pgaftest setup docs/tutorial/citus_tutorial.pgaf --tmux +# +# The setup block waits until every formation has a primary and a secondary. +# With --tmux you immediately get: +# +# top — docker compose logs -f (live container output) +# middle — pg_autoctl watch (all-node state dashboard) +# bottom — bash in the coordinator primary +# +# Things to try from the bottom pane: +# +# # check cluster state +# pg_autoctl show state \ +# --monitor postgresql://autoctl_node@monitor/pg_auto_failover +# +# # connect to the Citus coordinator +# psql -h coord0a -U postgres -d citus +# +# # trigger a coordinator failover +# pg_autoctl perform failover --formation coord \ +# --monitor postgresql://autoctl_node@monitor/pg_auto_failover +# +# # trigger a worker failover in group 1 +# pg_autoctl perform failover --group 1 \ +# --monitor postgresql://autoctl_node@monitor/pg_auto_failover +# +# When done: +# pgaftest down --work-dir /tmp/pgaftest/citus_tutorial + +cluster { + monitor + + formation coord { + coord0a coordinator + coord0b coordinator + } + + formation workers num-sync 1 { + worker1a worker group 1 + worker1b worker group 1 + worker2a worker group 2 + worker2b worker group 2 + worker3a worker group 3 + worker3b worker group 3 + } +} + +setup { + wait until primary, secondary timeout 180s +} + +teardown { + compose down +} diff --git a/docs/tutorial/interactive_tutorial.pgaf b/docs/tutorial/interactive_tutorial.pgaf new file mode 100644 index 000000000..8dbc146db --- /dev/null +++ b/docs/tutorial/interactive_tutorial.pgaf @@ -0,0 +1,48 @@ +# Interactive tutorial cluster +# +# A two-node HA cluster ready for hands-on exploration. There are no test +# steps — this spec is only meant to be used with `pgaftest setup --tmux`. +# +# Usage: +# pgaftest setup tests/tap/specs/interactive_tutorial.pgaf --tmux +# +# The setup block waits until you have a healthy primary + secondary, then +# pgaftest hands control to you. The three tmux panes let you observe the +# cluster in real time: +# +# top — docker compose logs -f (live container logs) +# middle — pg_autoctl watch (node state dashboard) +# bottom — bash in node1 (run pg_autoctl / psql commands) +# +# When you're done: +# pgaftest down --work-dir /tmp/pgaftest/interactive_tutorial +# +# Things to try from the bottom pane: +# +# # connect to the primary +# psql -h node1 -U postgres +# +# # trigger a manual failover +# pg_autoctl perform failover --monitor postgresql://autoctl_node@monitor/pg_auto_failover +# +# # watch the FSM transition live (middle pane updates automatically) +# +# # reconnect to the new primary after failover +# psql -h node2 -U postgres + +cluster { + monitor + formation { + node1 + node2 + } +} + +setup { + wait until primary, secondary timeout 120s + promote node1 +} + +teardown { + compose down +} From 6ff5939fe7917bd0ade1aca23b004d3ab7b6fbf2 Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Tue, 7 Jul 2026 01:10:39 +0200 Subject: [PATCH 19/68] pgaftest: fix setup --tmux timing, fix down --work-dir, update docs (code) --- .claude/pr-split-plan.md | 530 +++++++++++++++++++++++++++++++++ docs/citus-quickstart.rst | 77 +++++ docs/index.rst | 1 + docs/operations.rst | 52 ++++ docs/tutorial.rst | 40 +++ src/bin/pgaftest/cli_root.c | 72 ++++- src/bin/pgaftest/main.c | 3 + src/bin/pgaftest/test_runner.c | 88 ++++-- src/bin/pgaftest/test_runner.h | 6 + 9 files changed, 849 insertions(+), 20 deletions(-) create mode 100644 .claude/pr-split-plan.md diff --git a/.claude/pr-split-plan.md b/.claude/pr-split-plan.md new file mode 100644 index 000000000..60756a8a1 --- /dev/null +++ b/.claude/pr-split-plan.md @@ -0,0 +1,530 @@ +# PR Split Plan — pg_auto_failover #1125 + +Seven sequenced PRs extracted from the `pgaftest-infra` branch (48 commits, 158 files). +Each section is a self-contained brief for a new session. + +## Critical rebase note + +The commit history is NOT pre-split. The founding commit `a99e035` alone touches +common/, pg_autoctl commands, pgaftest, Dockerfile, CI, and specs simultaneously. +Later "fix CI" commits mix C source with spec changes in the same patch. + +**You cannot split by cherry-picking commits at existing boundaries.** +Each PR requires cherry-picking individual FILE changes out of mixed commits, +then squashing and cleaning up. Expect 2–4 hours of interactive rebase work +spread across sessions. + +Branch to split: `pgaftest-infra` +Base: `main` +Upstream: `hapostgres/pg_auto_failover` + +--- + +## PR 1 — CI modernisation & style check + +**Goal:** Update GitHub Actions dependencies and add an authoritative style-check job. +No source code changes. Merges first, unblocks everything else. + +### Files to change + +- `.github/workflows/run-tests.yml` +- `Makefile` (CI-related targets only: `citus_indent`, `banned` targets) +- `.gitattributes` (add `*.pgaf linguist-language=Ruby` or similar if present) +- `.gitignore` (add pgaftest build artefacts: `*.tab.c`, `*.tmp`) + +### What to do + +1. Create branch `ci-modernisation` off `main`. + +2. From `2a8efff` cherry-pick ONLY the `run-tests.yml` and `Makefile` CI changes: + ``` + git checkout pgaftest-infra -- .github/workflows/run-tests.yml + ``` + Then **revert** any lines that reference pgaftest-specific jobs + (those belong in PR 7's `run-pgaftest.yml`). + +3. Changes to make in `run-tests.yml`: + - `actions/checkout@v3` → `actions/checkout@v4.2.2` everywhere + - Remove the `PG14 linting` matrix entry + - Add a new `style_checker` job: + ```yaml + style_checker: + name: Style check + runs-on: ubuntu-latest + container: citus/stylechecker:no-py + steps: + - uses: actions/checkout@v4.2.2 + - run: git config --global --add safe.directory ${GITHUB_WORKSPACE} + - run: citus_indent --check + - run: ci/banned.h.sh + ``` + - Add PG17 to the PGVERSION matrix (was missing, only 13–16+18) + - Remove `TRAVIS_BUILD_DIR` env var export (dead code from old CI system) + +4. From `639913a` cherry-pick the `.gitignore` additions only. + +5. From `678c43e` / `d4acb65` cherry-pick `.gitattributes` if present. + +6. Verify: `git diff main HEAD --stat` should show only CI/meta files. + +### PR description + +"Update GitHub Actions to v4, add authoritative citus_indent style-check job +using `citus/stylechecker:no-py`, add PG17 to test matrix, remove TRAVIS_BUILD_DIR +remnants. No source changes." + +--- + +## PR 2 — Bug fixes in pg_autoctl (backportable) + +**Goal:** Ship the targeted C fixes independently. These fix real production bugs +and can be backported to v2.1. No structural changes. + +### Files to change + +- `src/bin/pg_autoctl/cli_common.c` +- `src/bin/pg_autoctl/cli_drop_node.c` + `file_utils.h` include +- `src/bin/pg_autoctl/cli_create_node.c` +- `src/bin/pg_autoctl/service_keeper.c` (minimal) +- `src/bin/pg_autoctl/fsm_transition.c` (if part of a173911) + +### What to do + +1. Create branch `fix-pg-autoctl-bugs` off `main`. + +2. Cherry-pick commit `7c093af` entirely (it's clean: SIGHUP fix, report_lsn timing, drop timeout): + ``` + git cherry-pick 7c093af + ``` + +3. Cherry-pick commit `afe91c3` entirely (drop-node state file): + ``` + git cherry-pick afe91c3 + ``` + +4. Cherry-pick commit `504c6b7` entirely (hba-lan pgSetup defer): + ``` + git cherry-pick 504c6b7 + ``` + +5. From `a173911` cherry-pick ONLY the C source changes (NOT Dockerfile or spec changes): + ``` + git checkout pgaftest-infra -- src/bin/pg_autoctl/fsm_transition.c + # verify it's only the upgrade build fix, not touching anything else + git diff HEAD + ``` + +6. Run style check: `docker run --rm -v $(pwd):/mnt citus/stylechecker:no-py citus_indent --check` + +### Tests to verify + +These fixes are validated by the existing Python test suite (`Run Tests` workflow). +The specific failing scenarios: +- SIGHUP fix: `test_basic_operation.py` reload step +- Drop-node fix: `test_basic_operation.py` drop secondary step +- hba-lan fix: any test that exercises `pg_autoctl create postgres --auth lan` + +### PR description + +"Three backportable bug fixes: +1. SIGHUP PID-reuse race in `pg_autoctl reload` — `signal(SIGHUP, SIG_IGN)` at + start of `cli_pg_autoctl_reload` prevents the broadcast SIGHUP from killing + a freshly exec'd one-shot command (exit 129). +2. Drop-node fails when service already cleaned up state file — add `else if` + branch in `cli_drop_local_node` that treats a missing state file as confirmation + of successful drop (pid ≠ 0 but state file gone). +3. hba-lan: defer pgSetup init until after config file exists." + +--- + +## PR 3 — Remove Azure integration + +**Goal:** Delete ~3 500 lines of unmaintained Azure scaffolding. Pure deletion, +no logic changes, unblocks the command reorganisation in PR 4. + +### Files to delete entirely + +``` +src/bin/pg_autoctl/azure.c +src/bin/pg_autoctl/azure.h +src/bin/pg_autoctl/azure_config.c +src/bin/pg_autoctl/azure_config.h +src/bin/pg_autoctl/cli_do_azure.c +src/bin/pg_autoctl/cli_do_tmux_azure.c +``` + +### Files to modify + +- `src/bin/pg_autoctl/cli_do_root.c` — remove azure subcommand registrations +- `src/bin/pg_autoctl/cli_do_root.h` — remove azure extern declarations +- `src/bin/pg_autoctl/Makefile` — remove azure*.c and cli_do_azure.c from OBJS + +### What to do + +1. Create branch `remove-azure` off `main` (or off PR 2 if that's merged first — + doesn't matter, no file overlap). + +2. Extract the azure deletions from `a99e035`: + ``` + git rm src/bin/pg_autoctl/azure.c src/bin/pg_autoctl/azure.h \ + src/bin/pg_autoctl/azure_config.c src/bin/pg_autoctl/azure_config.h \ + src/bin/pg_autoctl/cli_do_azure.c src/bin/pg_autoctl/cli_do_tmux_azure.c + ``` + +3. Edit `cli_do_root.c` to remove the azure extern and subcommand table entry. + Look for `azure_commands` and `cli_do_tmux_azure_commands` references. + +4. Edit `cli_do_root.h` to remove azure extern declarations. + +5. Edit `Makefile` to remove azure sources from `OBJS`. + +6. Build check: `make -C src/bin/pg_autoctl` should compile cleanly. + +### PR description + +"Remove unmaintained Azure integration (~3 500 lines). The `pg_autoctl do azure` +and `pg_autoctl do tmux azure` subcommands are deleted with no replacement — +the Azure tutorial was already broken and the code had no active users." + +--- + +## PR 4 — pg_autoctl command reorganisation (do → inspect / manual) + +**Goal:** Rename `do` to `internal` (stays hidden, subprocess-only entry points). +Add always-visible `inspect` (read-only diagnostics) and `manual` (FSM override ops) +subcommand trees. Update Python tests. Docs. + +**Depends on:** PR 3 merged (azure entries gone from `cli_do_root.c`). + +### New files + +- `src/bin/pg_autoctl/cli_inspect.c` + `.h` +- `src/bin/pg_autoctl/cli_manual.c` + `.h` + +### Modified files + +- `src/bin/pg_autoctl/cli_do_root.c` + `.h` +- `src/bin/pg_autoctl/cli_root.c` +- `src/bin/pg_autoctl/cli_do_fsm.c` (dispatch table name changes) +- `src/bin/pg_autoctl/cli_do_misc.c` +- `src/bin/pg_autoctl/cli_do_monitor.c` +- `src/bin/pg_autoctl/cli_do_service.c` +- `src/bin/pg_autoctl/cli_do_coordinator.c` +- `src/bin/pg_autoctl/cli_get_set_properties.c` (moved under inspect/manual) +- `docs/ref/pg_autoctl_do_pgsetup.rst` + +### What to do + +1. Create branch `pg-autoctl-commands` off PR 3 (or rebase onto main once PR 3 merges). + +2. The three relevant commits (`a99e035` command parts, `22ab675`, `bfd0dc4`, `21560e4`) + went through: `override` → `manual` → `internal` for the hidden tree. + The final state is: + - `pg_autoctl inspect …` — always visible, read-only diagnostics + - `pg_autoctl manual …` — always visible, FSM override operations + - `pg_autoctl internal …` — hidden, subprocess entry points for the supervisor + +3. Extract the command files from `pgaftest-infra`: + ``` + git checkout pgaftest-infra -- \ + src/bin/pg_autoctl/cli_inspect.c \ + src/bin/pg_autoctl/cli_inspect.h \ + src/bin/pg_autoctl/cli_manual.c \ + src/bin/pg_autoctl/cli_manual.h \ + src/bin/pg_autoctl/cli_do_root.c \ + src/bin/pg_autoctl/cli_do_root.h \ + src/bin/pg_autoctl/cli_root.c \ + src/bin/pg_autoctl/cli_do_fsm.c \ + src/bin/pg_autoctl/cli_do_misc.c \ + src/bin/pg_autoctl/cli_do_monitor.c \ + src/bin/pg_autoctl/cli_do_service.c \ + src/bin/pg_autoctl/cli_do_coordinator.c \ + src/bin/pg_autoctl/cli_get_set_properties.c \ + docs/ref/pg_autoctl_do_pgsetup.rst + ``` + Then manually verify each file: remove any pgaftest-specific references + (e.g. references to `pgaftest` in Makefile, any `cli_override` leftovers). + +4. Add `cli_inspect.c` and `cli_manual.c` to `src/bin/pg_autoctl/Makefile` OBJS. + +5. Run: `pg_autoctl --help` should show `inspect` and `manual` at top level. + `pg_autoctl internal --help` should be hidden (not in top-level help). + +6. Update Python tests: grep for `pg_autoctl do ` → `pg_autoctl inspect ` + or `pg_autoctl manual ` as appropriate. Commits `2a8efff` has the test changes. + +### PR description + +"Reorganise pg_autoctl command surface: +- `pg_autoctl inspect` (new, always visible): read-only diagnostics — wraps the + old `do show`, `do pgsetup`, `do monitor`, `do service getpid` subcommands +- `pg_autoctl manual` (new, always visible): FSM override operations — wraps the + old `do fsm assign/step`, `do primary`, `do standby`, `do coordinator` subcommands +- `pg_autoctl internal` (hidden): subprocess entry points for the supervisor + (was `do`, stays hidden, same purpose) +- `PG_AUTOCTL_DEBUG` no longer gates command visibility" + +--- + +## PR 5 — pg_autoctl node + node.ini + +**Goal:** New `pg_autoctl node` subcommand: declarative node spec file (`node.ini`) +and supervisor file-watcher that re-reads it on SIGHUP. + +**Depends on:** PR 4 merged (node hangs off the reorganised command tree). + +### New files + +- `src/bin/pg_autoctl/cli_node.c` + `.h` +- `src/bin/pg_autoctl/nodespec.c` + `.h` +- `docs/ref/pg_autoctl_node.rst` + +### Modified files + +- `src/bin/pg_autoctl/cli_root.c` (add node subcommand) +- `src/bin/pg_autoctl/keeper_config.c` + `.h` +- `src/bin/pg_autoctl/supervisor.c` + `.h` (SIGHUP handler) +- `src/bin/pg_autoctl/Makefile` (add cli_node.o, nodespec.o) +- `docs/index.rst` + +### What to do + +1. Create branch `pg-autoctl-node` off PR 4 (or rebase once PR 4 merges). + +2. Extract from `pgaftest-infra`: + ``` + git checkout pgaftest-infra -- \ + src/bin/pg_autoctl/cli_node.c \ + src/bin/pg_autoctl/cli_node.h \ + src/bin/pg_autoctl/nodespec.c \ + src/bin/pg_autoctl/nodespec.h \ + docs/ref/pg_autoctl_node.rst \ + docs/index.rst + ``` + +3. From `fe341da` extract the supervisor.c SIGHUP watcher changes and keeper_config changes. + Do NOT take the pgaftest-related parts of `c896747`. + +4. Add `cli_node.o nodespec.o` to `src/bin/pg_autoctl/Makefile`. + +5. Register `node_commands` in `cli_root.c` (visible, no PG_AUTOCTL_DEBUG gate). + +6. Verify: `pg_autoctl node --help` shows `run`, `show`, `edit` subcommands. + `pg_autoctl node run --pgdata /tmp/test` should read `node.ini` from PGDATA. + +### Note on optional deferral + +This PR can be deferred or kept merged with PR 4 if the node.ini work needs +more iteration. PR 4 is a self-contained rename and is shippable without PR 5. + +### PR description + +"Add `pg_autoctl node` subcommand tree: declarative node spec file (node.ini) +lets operators manage node configuration as a versioned file rather than a +sequence of `pg_autoctl set` commands. The supervisor file-watcher re-reads +node.ini on SIGHUP, applying changes without a full restart." + +--- + +## PR 6 — src/bin/common/ shared library build + +**Goal:** Move ~20 generic utility source files out of `src/bin/pg_autoctl/` into +`src/bin/common/` (a new static library). Both `pg_autoctl` and `pgaftest` link it. +Pure mechanical file moves — no logic changes. + +**Depends on:** PR 5 merged (pg_autoctl Makefile fully settled before adding common/). + +### Files being moved (git rename — no content changes) + +``` +debian.c/h env_utils.c/h file_utils.c/h +ini_file.c/h ini_implementation.c +ipaddr.c/h lock_utils.c/h +parsing.c/h pgctl.c/h pgsetup.c/h +pgsql.c/h pgtuning.c/h pidfile.c/h +signals.c/h string_utils.c/h system_utils.c/h +``` + +### New files + +- `src/bin/common/Makefile` +- `src/bin/common/Makefile.common` + +### Modified files + +- `src/bin/pg_autoctl/Makefile` — point to `../common/` for moved files, + link `../common/libpgaf_common.a` +- `src/bin/Makefile` — add `common` as first build target +- `src/bin/pgaftest/Makefile` — link `../common/libpgaf_common.a` + +### What to do + +1. Create branch `common-library` off PR 5 (or off main once PRs 1–5 merge). + +2. Cherry-pick `3d42128`: + ``` + git cherry-pick 3d42128 + ``` + This commit IS the file moves; git detects renames automatically. + But it also touches `pgaftest/Makefile` — that's OK since pgaftest build + references are inert until PR 7 adds the pgaftest source files. + +3. Cherry-pick `cabeb1e` (macOS build fix): + ``` + git cherry-pick cabeb1e + ``` + +4. Cherry-pick `b0d52e0` (dangling pointer warnings): + ``` + git cherry-pick b0d52e0 + ``` + +5. Verify pg_autoctl still builds: + ``` + make -C src/bin clean && make -C src/bin + ``` + The pg_autoctl binary should compile with headers resolved from `../common`. + +6. Check that `#include` paths in pg_autoctl source still resolve (they use + bare names like `#include "file_utils.h"` — the Makefile `-I ../common` flag + makes this work). + +### Rebase risk + +The `src/bin/pg_autoctl/Makefile` is heavily modified by PRs 3–5. +Rebase `common-library` on top of the merged PRs, resolve Makefile conflicts once. +The moved files themselves will not conflict (git rename detection is reliable here). + +### PR description + +"Move generic utility sources to src/bin/common/ static library. Both +`pg_autoctl` and `pgaftest` link against it. No logic changes — pure file +reorganisation to enable the pgaftest binary (PR 7) without duplicating sources." + +--- + +## PR 7 — pgaftest binary, DSL, test suite, upgrade test, CI workflow + +**Goal:** Everything pgaftest. The new `pgaftest` binary with its bison/flex DSL, +22 `.pgaf` spec files covering all Python test scenarios, the live-upgrade test +with the dual-binary Docker image, and the `run-pgaftest.yml` CI workflow. + +**Depends on:** PR 1 (CI workflow extends updated run-tests.yml) + PR 6 (pgaftest +links against src/bin/common/). + +### New files + +**Binary source** (`src/bin/pgaftest/`): +- `main.c`, `cli_root.c` — pgaftest CLI dispatch +- `test_spec.h` — AST node structs +- `test_spec_scan.l` — Flex lexer +- `test_spec_parse.y` — Bison grammar (pre-generated `.c/.h` files committed) +- `test_spec_parse.c/h`, `test_spec_scan.c` — committed bison/flex output +- `test_runner.c/h` — run/setup/step/down logic, TAP output, LISTEN loop +- `compose_gen.c/h` — Docker Compose YAML generator from cluster{} block +- `cli_demo.c/h` — demo app (moved from pg_autoctl do demo) +- `Makefile` + +**Spec files** (`tests/tap/specs/`): 22 `.pgaf` files + `tests/tap/schedule` + +**Upgrade test** (`tests/upgrade/`): +- `Dockerfile.current` — dual-binary image (v2.1 + v2.2) +- `Makefile` — `pgaf-current` and `pgaf-next` targets +- `install-extension.sh` — extension file installer baked into image +- `pg_autoctl_shim.sh` — translates `do service` → `internal service` after symlink flip + +**CI workflow**: `.github/workflows/run-pgaftest.yml` + +**Dockerfile**: add `pgaftest` and `debian` multi-stage targets, guard bison touch step + +**Docs**: `docs/pgaftest.rst` (1 071 lines), `docs/ref/manual.rst` update + +### What to do + +1. Create branch `pgaftest` off PR 6 (or off main once PRs 1–6 merge). + +2. The cleanest approach: take the full pgaftest-infra branch, then remove + everything that belongs in PRs 1–6, leaving only pgaftest-specific changes: + ``` + git checkout pgaftest-infra + git rebase -i main # squash all 48 commits into ~8 thematic ones + ``` + Then rebase the result onto PR 6. + +3. **Squash strategy** — 8 clean commits for this PR: + - `pgaftest: build system and Makefile` — Makefile + common/ link + - `pgaftest: DSL — lexer, parser, AST` — .l, .y, generated .c/.h, test_spec.h + - `pgaftest: runner and CLI` — test_runner.c, cli_root.c, compose_gen.c, main.c + - `pgaftest: spec suite` — all 22 .pgaf files + schedule + - `pgaftest: upgrade test` — tests/upgrade/ additions + - `pgaftest: Dockerfile targets` — pgaftest + debian stages, bison touch guard + - `pgaftest: GitHub Actions workflow` — run-pgaftest.yml + - `pgaftest: docs` — pgaftest.rst, manual.rst + +4. **Bison touch guard** (important): the Dockerfile must touch pre-generated + files BEFORE `make` so bison doesn't regenerate them (CI bison version may differ). + Current correct form: + ```dockerfile + RUN if [ -d src/bin/pgaftest ]; then \ + touch src/bin/pgaftest/test_spec_parse.c \ + src/bin/pgaftest/test_spec_parse.h \ + src/bin/pgaftest/test_spec_scan.c; \ + fi + ``` + The `-d` guard is needed because the upgrade test builds from a `git archive v2.1` + that does not have `src/bin/pgaftest/` at all. + +5. **Upgrade test image build**: before the CI run, the upgrade images must be built: + ``` + make -C tests/upgrade pgaf-current # builds pgaf:current (v2.1 + v2.2 binaries) + make -C tests/upgrade pgaf-next # builds pgaf:next (current branch) + ``` + This is automated in the `run-pgaftest.yml` workflow under the `upgrade` job. + +6. Verify the full pgaftest suite locally (PG17 default): + ``` + make pgaftest-image # build pgaftest Docker image + pgaftest run tests/tap/specs/basic_operation.pgaf + pgaftest run tests/tap/specs/multi_async.pgaf + ``` + +7. CI target: `pgaftest workflow` should show 22/22 green. The known pre-existing + failures (PG18, flaky Python tests) are in `Run Tests`, not here. + +### Key bugs already fixed (do NOT regress) + +These are in the current `pgaftest-infra` branch and must be carried into PR 7: +- `c3ae322` — trust notify convergence without subprocess double-check (multi_async fix) +- `5aba400` — poll monitor directly when LISTEN notifications missed (upgrade fix) +- `2185732` / `518d38f` — Dockerfile bison touch explicit paths + guard +- `7c093af` — SIGHUP race (already in PR 2; don't duplicate, just ensure it's in main first) + +### PR description + +"Add pgaftest: a new test binary and `.pgaf` spec language for deterministic +cluster testing. Replaces the Python nose test suite for CI, while also supporting +interactive cluster setup (`pgaftest setup spec.pgaf`). + +- 22 spec files covering all current Python test scenarios +- Live upgrade test: binary + extension swap without container restart +- GitHub Actions workflow: 22 parallel jobs, each spec in its own Docker Compose network +- TAP output: compatible with `prove` and standard CI TAP consumers" + +--- + +## Merge order summary + +``` +main ──┬── PR 1 (CI) ──────────────────────────────────────────┐ + └── PR 2 (bug fixes) │ + └── PR 3 (remove azure) ─────────────────────────────────┤ + └── PR 4 (commands) ─────────────────────────────── ┤ + └── PR 5 (node) ───────────────────────────── ┤ + └── PR 6 (common/) ─────────────────────┤ + └── PR 7 (pgaftest) ◄───────┘ + (also needs PR 1) +``` + +PRs 1 and 2 have no dependencies and can be opened simultaneously. +PR 3 can open once PR 1's style check passes (or in parallel — no file overlap). +PRs 4 → 5 → 6 → 7 are a linear chain. diff --git a/docs/citus-quickstart.rst b/docs/citus-quickstart.rst index f4b20e20a..19aae76f2 100644 --- a/docs/citus-quickstart.rst +++ b/docs/citus-quickstart.rst @@ -474,6 +474,83 @@ node too: ------- 75 +.. _citus_tutorial_pgaftest: + +Alternative: Citus cluster with pgaftest +----------------------------------------- + +The named Citus cluster from the previous section can also be started with a +single ``pgaftest`` command. ``pgaftest`` reads the ``.pgaf`` spec below, +generates the compose file, and opens a tmux session once all nodes are +healthy — no manual ``docker compose up`` or ``pg_autoctl watch`` needed. + +.. code-block:: text + :caption: docs/tutorial/citus_tutorial.pgaf + + # Citus cluster: one coordinator pair + three worker pairs + # + # Usage: + # pgaftest setup docs/tutorial/citus_tutorial.pgaf --tmux + + cluster { + monitor + + formation coord { + coord0a coordinator + coord0b coordinator + } + + formation workers num-sync 1 { + worker1a worker group 1 + worker1b worker group 1 + worker2a worker group 2 + worker2b worker group 2 + worker3a worker group 3 + worker3b worker group 3 + } + } + + setup { + wait until primary, secondary timeout 180s + } + + teardown { + compose down + } + +Start it: + +:: + + $ pgaftest setup docs/tutorial/citus_tutorial.pgaf --tmux + +Three panes open immediately: + +- **top** — ``docker compose logs -f`` (streaming container output) +- **middle** — ``pg_autoctl watch`` showing all eight nodes converging +- **bottom** — interactive ``bash`` in the coordinator primary + +From the bottom pane, verify the cluster:: + + pg_autoctl show state \ + --monitor postgresql://autoctl_node@monitor/pg_auto_failover + +To trigger a coordinator failover:: + + pg_autoctl perform failover --formation coord \ + --monitor postgresql://autoctl_node@monitor/pg_auto_failover + +To trigger a worker failover in group 1:: + + pg_autoctl perform failover --group 1 \ + --monitor postgresql://autoctl_node@monitor/pg_auto_failover + +When done:: + + $ pgaftest down --work-dir /tmp/pgaftest/citus_tutorial + +For the full ``pgaftest`` reference see :ref:`pgaftest`. + Cleanup ------- diff --git a/docs/index.rst b/docs/index.rst index 4abe8fc70..6d2eafe79 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -68,6 +68,7 @@ __ https://github.com/hapostgres/pg_auto_failover ref/manual ref/configuration + ref/pgaftest .. toctree:: :hidden: diff --git a/docs/operations.rst b/docs/operations.rst index 60a6566a0..523e3cefd 100644 --- a/docs/operations.rst +++ b/docs/operations.rst @@ -498,3 +498,55 @@ nodes must wait until the monitor is ready. For the full property reference and mutability table see :ref:`pg_autoctl_node`. + + +.. _testing: + +Testing +------- + +pg_auto_failover ships ``pgaftest``, an integration test runner that drives +clusters through scenarios described in ``.pgaf`` spec files. Specs combine +a topology declaration (the Docker Compose layout) with a sequence of named +steps that exercise failover, network partitions, maintenance windows, and +more. + +**Run a spec in CI mode** — full headless run with TAP output:: + + pgaftest run tests/tap/specs/basic_operation.pgaf + +The runner generates a ``docker-compose.yml`` from the ``cluster {}`` block, +starts the stack, runs the ``setup {}`` block to wait for a healthy cluster, +executes each step in ``sequence``, runs ``teardown {}``, and removes the +stack. Exit code 0 means all steps passed. + +**Run the full test schedule** across all supported Postgres versions:: + + pgaftest run --schedule tests/tap/schedule + +**Bring up an interactive cluster** for manual exploration:: + + pgaftest setup tests/tap/specs/basic_operation.pgaf + +**Add** ``--tmux`` to open a three-pane session the moment the compose +stack is ready — no waiting at a blank terminal. The setup block runs in +the bottom pane so you can watch the logs pane while the cluster initialises: + +- **top** — ``docker compose logs -f`` +- **middle** — ``pg_autoctl watch`` +- **bottom** — setup progress, then an interactive ``bash`` shell + +:: + + pgaftest setup tests/tap/specs/basic_operation.pgaf --tmux + +**Run a single named step** against a live cluster:: + + pgaftest step stop_primary --work-dir /tmp/pgaftest/basic_operation + +**Tear down** when done:: + + pgaftest down --work-dir /tmp/pgaftest/basic_operation + +The complete spec language reference, including all DSL commands, environment +variables, and TAP output format, is at :ref:`pgaftest`. diff --git a/docs/tutorial.rst b/docs/tutorial.rst index f9917c3b5..127b23973 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -366,6 +366,46 @@ To dispose of the entire tutorial environment, just use the following command: $ docker compose down +.. _tutorial_pgaftest: + +Alternative: interactive cluster with pgaftest +---------------------------------------------- + +The same two-node cluster from this tutorial can be started with a single +``pgaftest`` command, without writing any compose files or ini files by hand. +``pgaftest`` generates both from the spec file, starts the cluster, and opens +a tmux session so you can explore it immediately. + +The following spec file describes the tutorial topology: + +.. literalinclude:: tutorial/interactive_tutorial.pgaf + :language: text + :caption: docs/tutorial/interactive_tutorial.pgaf + +Start it with: + +:: + + $ pgaftest setup docs/tutorial/interactive_tutorial.pgaf --tmux + +Three tmux panes open as soon as the cluster is healthy: + +- **top** — ``docker compose logs -f`` (live container output) +- **middle** — ``pg_autoctl watch`` state dashboard +- **bottom** — interactive ``bash`` in ``node1`` + +From the bottom pane you can trigger a failover:: + + pg_autoctl perform failover \ + --monitor postgresql://autoctl_node@monitor/pg_auto_failover + +Watch the middle pane as the FSM transitions unfold in real time. When you +are done, tear down the cluster from any shell:: + + $ pgaftest down --work-dir /tmp/pgaftest/interactive_tutorial + +For the full ``pgaftest`` reference see :ref:`pgaftest`. + Next steps ---------- diff --git a/src/bin/pgaftest/cli_root.c b/src/bin/pgaftest/cli_root.c index 177f1a806..b35cfd6e7 100644 --- a/src/bin/pgaftest/cli_root.c +++ b/src/bin/pgaftest/cli_root.c @@ -481,10 +481,27 @@ cli_down(int argc, char **argv) spec = parse_test_spec(specPath); } - /* If no spec, just run compose down */ + /* If no spec, derive project name from workDir and run targeted compose down */ if (!spec) { - int rc = system("docker compose down --volumes --remove-orphans 2>&1"); + const char *base = strrchr(pgaftestOpts.workDir, '/'); + const char *projectName = (base && *(base + 1)) ? base + 1 + : pgaftestOpts.workDir; + + if (projectName[0] == '\0') + { + log_error("Cannot determine project name: " + "provide --work-dir or a spec file path"); + exit(1); + } + + char cmd[2048]; + snprintf(cmd, sizeof(cmd), + "docker compose -p %s -f %s/docker-compose.yml " + "down --volumes --remove-orphans", + projectName, pgaftestOpts.workDir); + + int rc = system(cmd); exit(rc == 0 ? 0 : 1); } @@ -493,6 +510,49 @@ cli_down(int argc, char **argv) } +/* ----------------------------------------------------------------------- + * pgaftest _setup_ --work-dir + * + * Internal command: runs the setup{} block against an already-running + * compose stack. Invoked from the tmux bottom pane by runner_setup() + * so the user immediately gets the session while setup runs live. + * Not intended to be called directly by users. + * ----------------------------------------------------------------------- */ +static void +cli_run_setup_only(int argc, char **argv) +{ + if (argc < 1 || argv[0] == NULL) + { + log_error("Usage: pgaftest _setup_ [--work-dir ]"); + exit(1); + } + + strncpy(pgaftestOpts.specFile, argv[0], + sizeof(pgaftestOpts.specFile) - 1); + +#ifdef __BSD_VISIBLE + optreset = 1; +#endif + optind = 1; + pgaftest_getopts(argc, argv); + + if (pgaftestOpts.workDir[0] == '\0') + { + derive_work_dir(pgaftestOpts.specFile, + pgaftestOpts.workDir, sizeof(pgaftestOpts.workDir)); + } + + TestSpec *spec = parse_test_spec(pgaftestOpts.specFile); + if (!spec) + { + exit(1); + } + + bool ok = runner_run_setup_only(spec, pgaftestOpts.workDir); + exit(ok ? 0 : 1); +} + + /* ----------------------------------------------------------------------- * Root command table * ----------------------------------------------------------------------- */ @@ -561,6 +621,13 @@ static CommandLine indent_command = "", pgaftest_getopts, cli_indent); +static CommandLine internal_setup_command = + make_command("_setup_", + "Internal: run setup{} block against a running stack (tmux helper)", + " [--work-dir ]", + "", + pgaftest_getopts, cli_run_setup_only); + static CommandLine *root_subcommands[] = { &run_command, &setup_command, @@ -569,6 +636,7 @@ static CommandLine *root_subcommands[] = { &prepare_command, &down_command, &indent_command, + &internal_setup_command, &pgaftest_demo_command, NULL }; diff --git a/src/bin/pgaftest/main.c b/src/bin/pgaftest/main.c index a1a90a1ea..31b0093db 100644 --- a/src/bin/pgaftest/main.c +++ b/src/bin/pgaftest/main.c @@ -41,6 +41,9 @@ CommandLine root_with_debug = make_command("pgaftest", "", "", "", NULL, NULL); int main(int argc, char **argv) { + /* store binary path so runner_setup can reference it in tmux panes */ + strlcpy(pg_autoctl_program, argv[0], sizeof(pg_autoctl_program)); + /* default log level: INFO to stderr */ log_set_level(LOG_INFO); diff --git a/src/bin/pgaftest/test_runner.c b/src/bin/pgaftest/test_runner.c index 3459894ee..087441d06 100644 --- a/src/bin/pgaftest/test_runner.c +++ b/src/bin/pgaftest/test_runner.c @@ -17,6 +17,9 @@ #include "log.h" #include "compose_gen.h" + +/* binary path set by main() for use in tmux pane commands */ +extern char pg_autoctl_program[]; #include "pgsql.h" #include "parsing.h" #include "nodestate_utils.h" @@ -3567,19 +3570,6 @@ runner_setup(TestSpec *spec, const char *workDir, bool withTmux) return false; } - /* run setup{} block */ - if (spec->setup) - { - char err[512] = ""; - log_info("Running setup block"); - if (!runner_exec_step(&r, spec->setup, err, sizeof(err), 0)) - { - log_error("Setup failed: %s", err); - runner_compose_down(&r); - return false; - } - } - if (withTmux) { /* @@ -3609,11 +3599,32 @@ runner_setup(TestSpec *spec, const char *workDir, bool withTmux) * Build a three-pane tmux session: * top — docker compose logs -f * middle — pg_autoctl watch on the monitor - * bottom — interactive bash in the first data node + * bottom — setup{} block running via `pgaftest _setup_`, then bash + * + * The setup block runs inside the bottom tmux pane so the user + * immediately gets the session and can watch logs while the cluster + * initialises. When setup completes the pane becomes a bash shell + * in the first data node. * - * Created detached first, then immediately attached so the current - * terminal lands inside the session. + * pg_autoctl_program holds argv[0] — the path to the pgaftest binary. */ + char bottomCmd[2048]; + + if (spec->setup) + { + snprintf(bottomCmd, sizeof(bottomCmd), + "%s _setup_ %s --work-dir %s && " + "%s exec -it %s bash", + pg_autoctl_program, spec->filename, workDir, + r.composeBase, shellNode); + } + else + { + snprintf(bottomCmd, sizeof(bottomCmd), + "%s exec -it %s bash", + r.composeBase, shellNode); + } + log_info("Starting tmux session \"%s\" (shell target: %s)", r.projectName, shellNode); @@ -3623,18 +3634,31 @@ runner_setup(TestSpec *spec, const char *workDir, bool withTmux) "split-window -v " "\"%s exec %s pg_autoctl watch\" \\; " "split-window -v " - "\"%s exec -it %s bash\" \\; " + "\"%s\" \\; " "select-layout even-vertical", r.projectName, r.composeBase, r.composeBase, r.activeMonitorService, - r.composeBase, shellNode); + bottomCmd); /* Attach the current terminal into the session. */ run_cmd("tmux attach-session -t %s", r.projectName); } else { + /* run setup{} block synchronously when not using tmux */ + if (spec->setup) + { + char err[512] = ""; + log_info("Running setup block"); + if (!runner_exec_step(&r, spec->setup, err, sizeof(err), 0)) + { + log_error("Setup failed: %s", err); + runner_compose_down(&r); + return false; + } + } + printf("\nCluster ready — compose project: %s\n", r.projectName); printf("Work dir: %s\n", workDir); printf("\nAvailable steps:"); @@ -3680,6 +3704,34 @@ runner_step(TestSpec *spec, const char *workDir, const char *stepName) } +bool +runner_run_setup_only(TestSpec *spec, const char *workDir) +{ + TestRunner r; + runner_init(&r, spec, workDir); + + if (!runner_load_state(&r)) + { + return false; + } + + if (!spec->setup) + { + return true; + } + + char err[512] = ""; + log_info("Running setup block"); + if (!runner_exec_step(&r, spec->setup, err, sizeof(err), 0)) + { + log_error("Setup failed: %s", err); + return false; + } + + return true; +} + + bool runner_down(TestSpec *spec, const char *workDir) { diff --git a/src/bin/pgaftest/test_runner.h b/src/bin/pgaftest/test_runner.h index 7199bd7a4..6aad159ea 100644 --- a/src/bin/pgaftest/test_runner.h +++ b/src/bin/pgaftest/test_runner.h @@ -73,6 +73,12 @@ bool runner_run(TestSpec *spec, const char *workDir, bool noCleanup); */ bool runner_setup(TestSpec *spec, const char *workDir, bool withTmux); +/* + * Run only the setup{} block against an already-running compose stack. + * Used internally by `pgaftest _setup_` (the tmux bottom-pane helper). + */ +bool runner_run_setup_only(TestSpec *spec, const char *workDir); + /* * Run a single named step against an already-running compose stack. * Used by `pgaftest step `. From d854104a5f034e5dd140873b65bc949119f26179 Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Tue, 7 Jul 2026 01:24:06 +0200 Subject: [PATCH 20/68] style: fix build-time warnings Dockerfile: replace deprecated apt-key with gpg --dearmor Three stages (base/build, run, testrun) were using: curl ... | apt-key add - which triggers a deprecation warning in recent apt versions. Replaced with the modern approach: gpg --dearmor -o /usr/share/keyrings/pgdg-archive-keyring.gpg and added signed-by= to the sources list entry. C: add missing prototype declarations - pgsql_alter_role_password() declared in common/pgsql.h was not visible when pg_autoctl/pgsql.h was included first; added to pg_autoctl/pgsql.h so the declaration is found regardless of include path ordering. - keeper_cli_pgsetup_hba_lan() in cli_do_misc.c was missing from cli_do_root.h; added alongside the other pgsetup helpers. --- Dockerfile | 15 +++++++++------ src/bin/pg_autoctl/cli_do_root.h | 1 + src/bin/pg_autoctl/pgsql.h | 3 +++ 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index 091f8134f..4222e73f0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -63,8 +63,9 @@ RUN apt-get update \ postgresql-common \ && rm -rf /var/lib/apt/lists/* -RUN curl https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - -RUN echo "deb http://apt.postgresql.org/pub/repos/apt bullseye-pgdg main ${PGVERSION}" > /etc/apt/sources.list.d/pgdg.list +RUN curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc \ + | gpg --dearmor -o /usr/share/keyrings/pgdg-archive-keyring.gpg +RUN echo "deb [signed-by=/usr/share/keyrings/pgdg-archive-keyring.gpg] http://apt.postgresql.org/pub/repos/apt bullseye-pgdg main ${PGVERSION}" > /etc/apt/sources.list.d/pgdg.list # bypass initdb of a "main" cluster RUN echo 'create_main_cluster = false' | sudo tee -a /etc/postgresql-common/createcluster.conf @@ -170,8 +171,9 @@ RUN apt-get update \ libpq-dev \ && rm -rf /var/lib/apt/lists/* -RUN curl https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - -RUN echo "deb http://apt.postgresql.org/pub/repos/apt bullseye-pgdg main ${PGVERSION}" > /etc/apt/sources.list.d/pgdg.list +RUN curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc \ + | gpg --dearmor -o /usr/share/keyrings/pgdg-archive-keyring.gpg +RUN echo "deb [signed-by=/usr/share/keyrings/pgdg-archive-keyring.gpg] http://apt.postgresql.org/pub/repos/apt bullseye-pgdg main ${PGVERSION}" > /etc/apt/sources.list.d/pgdg.list # bypass initdb of a "main" cluster RUN echo 'create_main_cluster = false' | sudo tee -a /etc/postgresql-common/createcluster.conf @@ -277,8 +279,9 @@ RUN apt-get update \ && rm -rf /var/lib/apt/lists/* # PGDG repo — needed for the libpq version that pg_autoctl and pgaftest link against -RUN curl https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - -RUN echo "deb http://apt.postgresql.org/pub/repos/apt bullseye-pgdg main ${PGVERSION}" \ +RUN curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc \ + | gpg --dearmor -o /usr/share/keyrings/pgdg-archive-keyring.gpg +RUN echo "deb [signed-by=/usr/share/keyrings/pgdg-archive-keyring.gpg] http://apt.postgresql.org/pub/repos/apt bullseye-pgdg main ${PGVERSION}" \ > /etc/apt/sources.list.d/pgdg.list \ && apt-get update \ && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ diff --git a/src/bin/pg_autoctl/cli_do_root.h b/src/bin/pg_autoctl/cli_do_root.h index bc129ad04..aa60d3070 100644 --- a/src/bin/pg_autoctl/cli_do_root.h +++ b/src/bin/pg_autoctl/cli_do_root.h @@ -98,6 +98,7 @@ void keeper_cli_enable_synchronous_replication(int argc, char **argv); void keeper_cli_disable_synchronous_replication(int argc, char **argv); void keeper_cli_pgsetup_pg_ctl(int argc, char **argv); +void keeper_cli_pgsetup_hba_lan(int argc, char **argv); void keeper_cli_pgsetup_discover(int argc, char **argv); void keeper_cli_pgsetup_is_ready(int argc, char **argv); void keeper_cli_pgsetup_wait_until_ready(int argc, char **argv); diff --git a/src/bin/pg_autoctl/pgsql.h b/src/bin/pg_autoctl/pgsql.h index cd8387172..792ce7d57 100644 --- a/src/bin/pg_autoctl/pgsql.h +++ b/src/bin/pg_autoctl/pgsql.h @@ -393,5 +393,8 @@ bool pgsql_alter_extension_update_to(PGSQL *pgsql, bool parseTimeLineHistory(const char *filename, const char *content, IdentifySystem *system); +bool pgsql_alter_role_password(PGSQL *pgsql, const char *roleName, + const char *password); + #endif /* PGSQL_H */ From 9170315796c2f41cd8e9a5391e9900b249b9b837 Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Tue, 7 Jul 2026 01:25:23 +0200 Subject: [PATCH 21/68] docs: move pgaftest into the Manual Pages toctree --- docs/index.rst | 1 - docs/ref/manual.rst | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index 6d2eafe79..4abe8fc70 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -68,7 +68,6 @@ __ https://github.com/hapostgres/pg_auto_failover ref/manual ref/configuration - ref/pgaftest .. toctree:: :hidden: diff --git a/docs/ref/manual.rst b/docs/ref/manual.rst index d71e00d7b..f7be581fa 100644 --- a/docs/ref/manual.rst +++ b/docs/ref/manual.rst @@ -30,3 +30,4 @@ have their own manual page. pg_autoctl_reload pg_autoctl_status pg_autoctl_activate + pgaftest From 24ad4776ef5ee1e38eb55a0eb2b303d143ecffcb Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Tue, 7 Jul 2026 01:33:53 +0200 Subject: [PATCH 22/68] refactor: remove duplicate source files from src/bin/pg_autoctl/ The 16 .c/.h files that were moved to src/bin/common/ were still present in src/bin/pg_autoctl/ as copies. pg_autoctl was compiling them locally via the SRC = $(wildcard *.c) glob AND linking libpgaf_common.a, causing duplicate symbol resolution (local .o silently winning over the archive). All 32 files (16 .c + 16 .h pairs) are removed; pg_autoctl now uses only the common/ versions via libpgaf_common.a and its include path. The 6 files that had minor divergences were analysed: - ini_file.c / ipaddr.c / pgsetup.c / signals.c: only brace-style differences from citus_indent; common/ version is correct. - pgctl.c: common/ is a superset (adds --checkpoint=fast for pg_basebackup, fixes intToString dangling-pointer use). - pgsql.c: common/ is a superset (adds pgsql_alter_role_password). Both .c and .h pairs for pgctl and pgsql were checked; common/ contains all declarations. --- src/bin/pg_autoctl/debian.c | 717 ----- src/bin/pg_autoctl/debian.h | 68 - src/bin/pg_autoctl/env_utils.c | 166 -- src/bin/pg_autoctl/env_utils.h | 22 - src/bin/pg_autoctl/file_utils.c | 943 ------- src/bin/pg_autoctl/file_utils.h | 73 - src/bin/pg_autoctl/ini_file.c | 740 ----- src/bin/pg_autoctl/ini_file.h | 117 - src/bin/pg_autoctl/ini_implementation.c | 13 - src/bin/pg_autoctl/ipaddr.c | 920 ------ src/bin/pg_autoctl/ipaddr.h | 40 - src/bin/pg_autoctl/lock_utils.c | 382 --- src/bin/pg_autoctl/lock_utils.h | 40 - src/bin/pg_autoctl/parsing.c | 1221 -------- src/bin/pg_autoctl/parsing.h | 100 - src/bin/pg_autoctl/pgctl.c | 2851 ------------------- src/bin/pg_autoctl/pgctl.h | 79 - src/bin/pg_autoctl/pgsetup.c | 2039 -------------- src/bin/pg_autoctl/pgsetup.h | 282 -- src/bin/pg_autoctl/pgsql.c | 3390 ----------------------- src/bin/pg_autoctl/pgsql.h | 400 --- src/bin/pg_autoctl/pgtuning.c | 390 --- src/bin/pg_autoctl/pgtuning.h | 19 - src/bin/pg_autoctl/pidfile.c | 568 ---- src/bin/pg_autoctl/pidfile.h | 74 - src/bin/pg_autoctl/signals.c | 258 -- src/bin/pg_autoctl/signals.h | 38 - src/bin/pg_autoctl/string_utils.c | 601 ---- src/bin/pg_autoctl/string_utils.h | 44 - src/bin/pg_autoctl/system_utils.c | 145 - src/bin/pg_autoctl/system_utils.h | 27 - 31 files changed, 16767 deletions(-) delete mode 100644 src/bin/pg_autoctl/debian.c delete mode 100644 src/bin/pg_autoctl/debian.h delete mode 100644 src/bin/pg_autoctl/env_utils.c delete mode 100644 src/bin/pg_autoctl/env_utils.h delete mode 100644 src/bin/pg_autoctl/file_utils.c delete mode 100644 src/bin/pg_autoctl/file_utils.h delete mode 100644 src/bin/pg_autoctl/ini_file.c delete mode 100644 src/bin/pg_autoctl/ini_file.h delete mode 100644 src/bin/pg_autoctl/ini_implementation.c delete mode 100644 src/bin/pg_autoctl/ipaddr.c delete mode 100644 src/bin/pg_autoctl/ipaddr.h delete mode 100644 src/bin/pg_autoctl/lock_utils.c delete mode 100644 src/bin/pg_autoctl/lock_utils.h delete mode 100644 src/bin/pg_autoctl/parsing.c delete mode 100644 src/bin/pg_autoctl/parsing.h delete mode 100644 src/bin/pg_autoctl/pgctl.c delete mode 100644 src/bin/pg_autoctl/pgctl.h delete mode 100644 src/bin/pg_autoctl/pgsetup.c delete mode 100644 src/bin/pg_autoctl/pgsetup.h delete mode 100644 src/bin/pg_autoctl/pgsql.c delete mode 100644 src/bin/pg_autoctl/pgsql.h delete mode 100644 src/bin/pg_autoctl/pgtuning.c delete mode 100644 src/bin/pg_autoctl/pgtuning.h delete mode 100644 src/bin/pg_autoctl/pidfile.c delete mode 100644 src/bin/pg_autoctl/pidfile.h delete mode 100644 src/bin/pg_autoctl/signals.c delete mode 100644 src/bin/pg_autoctl/signals.h delete mode 100644 src/bin/pg_autoctl/string_utils.c delete mode 100644 src/bin/pg_autoctl/string_utils.h delete mode 100644 src/bin/pg_autoctl/system_utils.c delete mode 100644 src/bin/pg_autoctl/system_utils.h diff --git a/src/bin/pg_autoctl/debian.c b/src/bin/pg_autoctl/debian.c deleted file mode 100644 index e22118b4c..000000000 --- a/src/bin/pg_autoctl/debian.c +++ /dev/null @@ -1,717 +0,0 @@ -/* - * src/bin/pg_autoctl/debian.c - * - * Debian specific code to support registering a pg_autoctl node from a - * Postgres cluster created with pg_createcluster. We need to move the - * configuration files back to PGDATA. - * - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the PostgreSQL License. - * - */ -#include - -#include "postgres_fe.h" -#include "pqexpbuffer.h" - -#include "debian.h" -#include "keeper.h" -#include "keeper_config.h" -#include "parsing.h" - -#define EDITED_BY_PG_AUTOCTL "# edited by pg_auto_failover \n" - -static bool debian_find_postgres_configuration_files(PostgresSetup *pgSetup, - PostgresConfigFiles *pgConfigFiles); - -static bool debian_init_postgres_config_files(PostgresSetup *pgSetup, - PostgresConfigFiles *pgConfFiles, - PostgresConfigurationKind confKind); - -static bool buildDebianDataAndConfDirectoryNames(PostgresSetup *pgSetup, - DebianPathnames *debPathnames); - -static bool expandDebianPatterns(DebianPathnames *debPathnames, - const char *dataDirectoryTemplate, - const char *confDirectoryTemplate); - -static bool expandDebianPatternsInDirectoryName(char *pathname, - int pathnameSize, - const char *template, - const char *versionName, - const char *clusterName); - -static void initPostgresConfigFiles(const char *dirname, - PostgresConfigFiles *pgConfigFiles, - PostgresConfigurationKind kind); - -static bool postgresConfigFilesAllExist(PostgresConfigFiles *pgConfigFiles); - -static bool move_configuration_files(PostgresConfigFiles *src, - PostgresConfigFiles *dst); - -static bool comment_out_configuration_parameters(const char *srcConfPath, - const char *dstConfPath); - -static bool disableAutoStart(PostgresConfigFiles *pgConfigFiles); - - -/* - * keeper_ensure_pg_configuration_files_in_pgdata checks if postgresql.conf, - * pg_hba.conf, pg_ident.conf files exist in $PGDATA, if not it tries to get - * them from default location and modifies paths inside copied postgresql.conf. - */ -bool -keeper_ensure_pg_configuration_files_in_pgdata(PostgresSetup *pgSetup) -{ - PostgresConfigFiles pgConfigFiles = { 0 }; - - if (!debian_find_postgres_configuration_files(pgSetup, &pgConfigFiles)) - { - /* errors have already been logged */ - return false; - } - - switch (pgConfigFiles.kind) - { - case PG_CONFIG_TYPE_POSTGRES: - { - /* that's it, we're good */ - return true; - } - - case PG_CONFIG_TYPE_DEBIAN: - { - /* - * So now pgConfigFiles is the debian path for configuration files, - * and we're building a new pgdataConfigFiles for the Postgres - * configuration files in PGDATA. - */ - PostgresConfigFiles pgdataConfigFiles = { 0 }; - - log_info("Found a debian style installation in PGDATA \"%s\" with " - "postgresql.conf located at \"%s\"", - pgSetup->pgdata, - pgConfigFiles.conf); - - initPostgresConfigFiles(pgSetup->pgdata, - &pgdataConfigFiles, - PG_CONFIG_TYPE_POSTGRES); - - log_info("Moving configuration files back to PGDATA at \"%s\"", - pgSetup->pgdata); - - /* move configuration files back to PGDATA, or die trying */ - if (!move_configuration_files(&pgConfigFiles, - &pgdataConfigFiles)) - { - char *_dirname = dirname(pgConfigFiles.conf); - - log_fatal("Failed to move the debian configuration files from " - "\"%s\" back to PGDATA at \"%s\"", - _dirname, - pgSetup->pgdata); - return false; - } - - /* also disable debian auto start of the cluster we now own */ - if (!disableAutoStart(&pgConfigFiles)) - { - log_fatal("Failed to disable debian auto-start behavior, " - "see above for details"); - return false; - } - - return true; - } - - case PG_CONFIG_TYPE_UNKNOWN: - { - log_fatal("Failed to find the \"postgresql.conf\" file. " - "It's not in PGDATA, and it's not in the debian " - "place we had a look at. See above for details"); - return false; - } - } - - /* This is a huge bug */ - log_error("BUG: some unknown PG_CONFIG enum value was encountered"); - return false; -} - - -/* - * debian_find_postgres_configuration_files finds the Postgres configuration - * files following the following strategies: - * - * - first attempt to find the files where we expect them, in PGDATA - * - then attempt to find the files in the debian /etc/postgresql/%v/%c place - * - * At the moment we only have those two strategies, and with some luck that's - * all we're ever going to need. - */ -static bool -debian_find_postgres_configuration_files(PostgresSetup *pgSetup, - PostgresConfigFiles *pgConfigFiles) -{ - PostgresConfigFiles postgresConfFiles = { 0 }; - PostgresConfigFiles debianConfFiles = { 0 }; - - pgConfigFiles->kind = PG_CONFIG_TYPE_UNKNOWN; - - if (!pg_setup_pgdata_exists(pgSetup)) - { - return PG_CONFIG_TYPE_UNKNOWN; - } - - /* is it a Postgres core initdb style setup? */ - if (debian_init_postgres_config_files(pgSetup, - &postgresConfFiles, - PG_CONFIG_TYPE_POSTGRES)) - { - /* so we're dealing with a "normal" Postgres installation */ - *pgConfigFiles = postgresConfFiles; - - return true; - } - - /* - * Is it a debian postgresql-common style setup then? - * - * We only search for debian style setup when the main postgresql.conf file - * was not found. The previous call to debian_init_postgres_config_files - * might see a partial failure because of e.g. missing only pg_ident.conf. - */ - if (!file_exists(postgresConfFiles.conf)) - { - if (debian_init_postgres_config_files(pgSetup, - &debianConfFiles, - PG_CONFIG_TYPE_DEBIAN)) - { - /* so we're dealing with a "debian style" Postgres installation */ - *pgConfigFiles = debianConfFiles; - - return true; - } - } - - /* well that's all we know how to detect at this point */ - return false; -} - - -/* - * debian_init_postgres_config_files initializes the given PostgresConfigFiles - * structure with the location of existing files as found on-disk given a - * Postgres configuration kind. - */ -static bool -debian_init_postgres_config_files(PostgresSetup *pgSetup, - PostgresConfigFiles *pgConfigFiles, - PostgresConfigurationKind confKind) -{ - const char *pgdata = pgSetup->pgdata; - - switch (confKind) - { - case PG_CONFIG_TYPE_UNKNOWN: - { - /* that's a bug really */ - log_error("BUG: debian_init_postgres_config_files " - "called with UNKNOWN conf kind"); - return false; - } - - case PG_CONFIG_TYPE_POSTGRES: - { - initPostgresConfigFiles(pgdata, pgConfigFiles, - PG_CONFIG_TYPE_POSTGRES); - - return postgresConfigFilesAllExist(pgConfigFiles); - } - - case PG_CONFIG_TYPE_DEBIAN: - { - DebianPathnames debPathnames = { 0 }; - - if (!buildDebianDataAndConfDirectoryNames(pgSetup, &debPathnames)) - { - log_warn("Failed to match PGDATA at \"%s\" with a debian " - "setup following the data_directory template " - "'/var/lib/postgresql/%%v/%%c'", - pgSetup->pgdata); - return false; - } - - initPostgresConfigFiles(debPathnames.confDirectory, - pgConfigFiles, PG_CONFIG_TYPE_DEBIAN); - - return postgresConfigFilesAllExist(pgConfigFiles); - } - } - - /* This is a huge bug */ - log_error("BUG: some unknown PG_CONFIG enum value was encountered"); - return false; -} - - -/* - * buildDebianDataAndConfDirectoryNames builds the debian specific directory - * pathnames from the pgSetup pgdata location. - * - * For a debian cluster, we first have to extract the "cluster" name (%c) and - * then find the configuration files in /etc/postgresql/%v/%c with %v being the - * version number. - * - * Note that debian's /etc/postgresql-common/createcluster.conf defaults to - * using the following setup, and that's the only one we support at this - * moment. - * - * data_directory = '/var/lib/postgresql/%v/%c' - * - */ -static bool -buildDebianDataAndConfDirectoryNames(PostgresSetup *pgSetup, - DebianPathnames *debPathnames) -{ - char *pgmajor = strdup(pgSetup->pg_version); - - char pgdata[MAXPGPATH]; - - char clusterDir[MAXPGPATH] = { 0 }; - char versionDir[MAXPGPATH] = { 0 }; - - if (pgmajor == NULL) - { - log_error(ALLOCATION_FAILED_ERROR); - return false; - } - - /* we need to work with the absolute pathname of PGDATA */ - if (!normalize_filename(pgSetup->pgdata, pgdata, MAXPGPATH)) - { - /* errors have already been logged */ - return false; - } - - /* clusterDir is the same as pgdata really */ - strlcpy(clusterDir, pgdata, MAXPGPATH); - - /* from PGDATA, get the directory one-level up */ - strlcpy(versionDir, clusterDir, MAXPGPATH); - get_parent_directory(versionDir); - - /* get the names of our version and cluster directories */ - char *clusterDirName = strdup(basename(clusterDir)); - char *versionDirName = strdup(basename(versionDir)); - - if (clusterDirName == NULL || versionDirName == NULL) - { - log_error(ALLOCATION_FAILED_ERROR); - return false; - } - - /* transform pgversion "11.4" to "11" to get the major version part */ - char *dot = strchr(pgmajor, '.'); - - if (dot) - { - *dot = '\0'; - } - - /* check that debian pathname version string == Postgres version string */ - if (strcmp(versionDirName, pgmajor) != 0) - { - log_debug("Failed to match the version component of the " - "debian data_directory \"%s\" with the current " - "version of Postgres: \"%s\"", - pgdata, - pgmajor); - return false; - } - - /* prepare given debPathnames */ - strlcpy(debPathnames->versionName, versionDirName, PG_VERSION_STRING_MAX); - strlcpy(debPathnames->clusterName, clusterDirName, MAXPGPATH); - - if (!expandDebianPatterns(debPathnames, - "/var/lib/postgresql/%v/%c", - "/etc/postgresql/%v/%c")) - { - /* errors have already been logged */ - return false; - } - - /* free memory allocated with strdup */ - free(pgmajor); - free(clusterDirName); - free(versionDirName); - - return true; -} - - -/* - * expandDebianPatterns expands the %v and %c values in given templates and - * apply the result to debPathnames->dataDirectory and - * debPathnames->confDirectory. - */ -static bool -expandDebianPatterns(DebianPathnames *debPathnames, - const char *dataDirectoryTemplate, - const char *confDirectoryTemplate) -{ - return expandDebianPatternsInDirectoryName(debPathnames->dataDirectory, - MAXPGPATH, - dataDirectoryTemplate, - debPathnames->versionName, - debPathnames->clusterName) - - && expandDebianPatternsInDirectoryName(debPathnames->confDirectory, - MAXPGPATH, - confDirectoryTemplate, - debPathnames->versionName, - debPathnames->clusterName); -} - - -/* - * expandDebianPatternsInDirectoryName prepares a debian target data_directory - * or configuration directory from a pattern. - * - * Given the parameters: - * template = "/var/lib/postgresql/%v/%c" - * versionName = "11" - * clusterName = "main" - * - * Then the following string is copied in pre-allocated pathname: - * "/var/lib/postgresql/11/main" - */ -static bool -expandDebianPatternsInDirectoryName(char *pathname, - int pathnameSize, - const char *template, - const char *versionName, - const char *clusterName) -{ - int pathnameIndex = 0; - int templateIndex = 0; - int templateSize = strlen(template); - bool previousCharIsPercent = false; - - for (templateIndex = 0; templateIndex < templateSize; templateIndex++) - { - char currentChar = template[templateIndex]; - - if (pathnameIndex >= pathnameSize) - { - log_error("BUG: expandDebianPatternsInDirectoryName destination " - "buffer is too short (%d bytes)", pathnameSize); - return false; - } - - if (previousCharIsPercent) - { - switch (currentChar) - { - case 'v': - { - int versionSize = strlen(versionName); - - /* - * Only copy if we have enough room, increment pathnameSize - * anyways so that the first check in the main loop catches - * and report the error. - */ - if ((pathnameIndex + versionSize) < pathnameSize) - { - strlcpy(pathname + pathnameIndex, versionName, pathnameSize - - pathnameIndex); - pathnameIndex += versionSize; - } - break; - } - - case 'c': - { - int clusterSize = strlen(clusterName); - - /* - * Only copy if we have enough room, increment pathnameSize - * anyways so that the first check in the main loop catches - * and report the error. - */ - if ((pathnameIndex + clusterSize) < pathnameSize) - { - strlcpy(pathname + pathnameIndex, clusterName, pathnameSize - - pathnameIndex); - pathnameIndex += clusterSize; - } - break; - } - - default: - { - pathname[pathnameIndex++] = currentChar; - break; - } - } - } - else if (currentChar != '%') - { - pathname[pathnameIndex++] = currentChar; - } - - previousCharIsPercent = currentChar == '%'; - } - - return true; -} - - -/* - * initPostgresConfigFiles initializes PostgresConfigFiles structure with our - * filenames located in given directory pathname. - */ -static void -initPostgresConfigFiles(const char *dirname, - PostgresConfigFiles *pgConfigFiles, - PostgresConfigurationKind confKind) -{ - pgConfigFiles->kind = confKind; - join_path_components(pgConfigFiles->conf, dirname, "postgresql.conf"); - join_path_components(pgConfigFiles->ident, dirname, "pg_ident.conf"); - join_path_components(pgConfigFiles->hba, dirname, "pg_hba.conf"); -} - - -/* - * postgresConfigFilesAllExist returns true when the three files that we track - * all exit on the file system, per file_exists() test. - */ -static bool -postgresConfigFilesAllExist(PostgresConfigFiles *pgConfigFiles) -{ - /* - * WARN the user about the unexpected nature of our setup here, even if we - * then move on to make it the way we expect it. - */ - if (!file_exists(pgConfigFiles->conf)) - { - log_warn("Failed to find Postgres configuration files in PGDATA, " - "as expected: \"%s\" does not exist", - pgConfigFiles->conf); - } - - if (!file_exists(pgConfigFiles->ident)) - { - log_warn("Failed to find Postgres configuration files in PGDATA, " - "as expected: \"%s\" does not exist", - pgConfigFiles->ident); - } - - if (!file_exists(pgConfigFiles->hba)) - { - log_warn("Failed to find Postgres configuration files in PGDATA, " - "as expected: \"%s\" does not exist", - pgConfigFiles->hba); - } - - return file_exists(pgConfigFiles->conf) && - file_exists(pgConfigFiles->ident) && - file_exists(pgConfigFiles->hba); -} - - -/* - * move_configuration_files moves configuration files from the source place to - * the destination place as given. - * - * While moving the files, we also need to edit the "postgresql.conf" content - * to comment out the lines for the config_file, hba_file, and ident_file - * location. We're going to use the Postgres defaults in PGDATA. - */ -static bool -move_configuration_files(PostgresConfigFiles *src, PostgresConfigFiles *dst) -{ - /* edit postgresql.conf and move it to its dst pathname */ - log_info("Preparing \"%s\" from \"%s\"", dst->conf, src->conf); - - if (!comment_out_configuration_parameters(src->conf, dst->conf)) - { - return false; - } - - /* HBA and ident files are copied without edits */ - log_info("Moving \"%s\" to \"%s\"", src->hba, dst->hba); - - if (!move_file(src->hba, dst->hba)) - { - /* - * Clean-up the mess then, and return false whether the clean-up is a - * success or not. - */ - (void) unlink_file(dst->conf); - - return false; - } - - - /* HBA and ident files are copied without edits */ - log_info("Moving \"%s\" to \"%s\"", src->ident, dst->ident); - - if (!move_file(src->ident, dst->ident)) - { - /* - * Clean-up the mess then, and return false whether the clean-up is a - * success or not. - */ - (void) unlink_file(dst->conf); - (void) move_file(dst->hba, src->hba); - - return false; - } - - /* finish the move of the postgresql.conf */ - if (!unlink_file(src->conf)) - { - /* - * Clean-up the mess then, and return false whether the clean-up is a - * success or not. - */ - (void) move_file(dst->hba, src->hba); - (void) move_file(dst->ident, src->ident); - return false; - } - - /* consider failure to symlink as a non-fatal event */ - (void) create_symbolic_link(src->conf, dst->conf); - (void) create_symbolic_link(src->ident, dst->ident); - (void) create_symbolic_link(src->hba, dst->hba); - - return true; -} - - -/* - * comment_out_configuration_parameters reads postgresql.conf file from source - * location and writes a new version of it at destination location with some - * parameters commented out: - * - * data_directory - * config_file - * hba_file - * ident_file - * include_dir - */ -static bool -comment_out_configuration_parameters(const char *srcConfPath, - const char *dstConfPath) -{ - char lineBuffer[BUFSIZE]; - - /* - * configuration parameters can appear in any order, and we - * need to check for patterns for NAME = VALUE and NAME=VALUE - */ - char *targetVariableExpression = - "(" - "data_directory" - "|hba_file" - "|ident_file" - "|include_dir" - "|stats_temp_directory" - ")( *)="; - - /* open a file */ - FILE *fileStream = fopen_read_only(srcConfPath); - if (fileStream == NULL) - { - log_error("Failed to open file \"%s\": %m", srcConfPath); - return false; - } - - PQExpBuffer newConfContents = createPQExpBuffer(); - if (newConfContents == NULL) - { - log_error("Failed to allocate memory"); - return false; - } - - /* read each line including terminating new line and process it */ - while (fgets(lineBuffer, BUFSIZE, fileStream) != NULL) - { - bool variableFound = false; - char *matchedString = - regexp_first_match(lineBuffer, targetVariableExpression); - - /* check if the line contains any of target variables */ - if (matchedString != NULL) - { - variableFound = true; - - /* regexp_first_match uses malloc, result must be deallocated */ - free(matchedString); - } - - /* - * comment out the line if any of target variables is found - * and if it was not already commented - */ - if (variableFound && lineBuffer[0] != '#') - { - appendPQExpBufferStr(newConfContents, EDITED_BY_PG_AUTOCTL); - appendPQExpBufferStr(newConfContents, "# "); - } - - /* copy rest of the line */ - appendPQExpBufferStr(newConfContents, lineBuffer); - } - - fclose(fileStream); - - /* write the resulting content at the destination path */ - if (!write_file(newConfContents->data, newConfContents->len, dstConfPath)) - { - destroyPQExpBuffer(newConfContents); - return false; - } - - /* we don't need the buffer anymore */ - destroyPQExpBuffer(newConfContents); - - /* - * Refrain from removing the source file, we might fail to proceed and then - * we will want to offer a path forward to the user where the original - * configuration file is still around - */ - - return true; -} - - -/* - * disableAutoStart disables auto start in default configuration - */ -static bool -disableAutoStart(PostgresConfigFiles *pgConfigFiles) -{ - char startConfPath[MAXPGPATH] = { 0 }; - char copyStartConfPath[MAXPGPATH] = { 0 }; - char *newStartConfData = EDITED_BY_PG_AUTOCTL "disabled"; - - path_in_same_directory(pgConfigFiles->conf, "start.conf", startConfPath); - path_in_same_directory(pgConfigFiles->conf, - "start.conf.orig", copyStartConfPath); - - if (rename(startConfPath, copyStartConfPath) != 0) - { - log_error("Failed to rename debian auto start setup to \"%s\": %m", - copyStartConfPath); - - return false; - } - - return write_file(newStartConfData, strlen(newStartConfData), startConfPath); -} diff --git a/src/bin/pg_autoctl/debian.h b/src/bin/pg_autoctl/debian.h deleted file mode 100644 index 9db36c785..000000000 --- a/src/bin/pg_autoctl/debian.h +++ /dev/null @@ -1,68 +0,0 @@ -/* - * src/bin/pg_autoctl/debian.h - * - * Debian specific code to support registering a pg_autoctl node from a - * Postgres cluster created with pg_createcluster. We need to move the - * configuration files back to PGDATA. - * - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the PostgreSQL License. - * - */ - -#ifndef DEBIAN_H -#define DEBIAN_H - -#include "keeper_config.h" -#include "pgsetup.h" - -/* - * We know how to find configuration files in either PGDATA as per Postgres - * core, or in the debian cluster configuration directory as per debian - * postgres-common packaging, implemented in pg_createcluster. - */ -typedef enum -{ - PG_CONFIG_TYPE_UNKNOWN = 0, - PG_CONFIG_TYPE_POSTGRES, - PG_CONFIG_TYPE_DEBIAN -} PostgresConfigurationKind; - -/* - * debian's pg_createcluster moves the 3 configuration files to a place in /etc: - * - * - postgresql.conf - * - pg_ident.conf - * - pg_hba.conf - * - * On top of that debian also manages a "start.conf" file to decide if their - * systemd integration should manage a given cluster. - */ -typedef struct pg_config_files -{ - PostgresConfigurationKind kind; - char conf[MAXPGPATH]; - char ident[MAXPGPATH]; - char hba[MAXPGPATH]; -} PostgresConfigFiles; - - -/* - * debian handles paths for data_directory and configuration directory that - * depend on two components: Postgres version string ("11", "12", etc) and - * debian cluster name (defaults to "main"). - */ -typedef struct debian_pathnames -{ - char versionName[PG_VERSION_STRING_MAX]; - char clusterName[MAXPGPATH]; - - char dataDirectory[MAXPGPATH]; - char confDirectory[MAXPGPATH]; -} DebianPathnames; - - -bool keeper_ensure_pg_configuration_files_in_pgdata(PostgresSetup *pgSetup); - - -#endif /* DEBIAN_H */ diff --git a/src/bin/pg_autoctl/env_utils.c b/src/bin/pg_autoctl/env_utils.c deleted file mode 100644 index 2b1f22109..000000000 --- a/src/bin/pg_autoctl/env_utils.c +++ /dev/null @@ -1,166 +0,0 @@ -/* - * src/bin/pg_autoctl/env_utils.c - * Utility functions for interacting with environment settings. - * - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the PostgreSQL License. - * - */ - -#include -#include - -#include "defaults.h" -#include "env_utils.h" -#include "log.h" - - -/* - * env_found_empty returns true if the passed environment variable is the empty - * string. It returns false when the environment variable is not set or if it - * set but is something else than the empty string. - */ -bool -env_found_empty(const char *name) -{ - if (name == NULL || strlen(name) == 0) - { - log_error("Failed to get environment setting. " - "NULL or empty variable name is provided"); - return false; - } - - /* - * Explanation of IGNORE-BANNED - * getenv is safe here because we never provide null argument, - * and only check the value it's length. - */ - char *envvalue = getenv(name); /* IGNORE-BANNED */ - return envvalue != NULL && strlen(envvalue) == 0; -} - - -/* - * env_exists returns true if the passed environment variable exists in the - * environment, otherwise it returns false. - */ -bool -env_exists(const char *name) -{ - if (name == NULL || strlen(name) == 0) - { - log_error("Failed to get environment setting. " - "NULL or empty variable name is provided"); - return false; - } - - /* - * Explanation of IGNORE-BANNED - * getenv is safe here because we never provide null argument, - * and only check if it returns NULL. - */ - return getenv(name) != NULL; /* IGNORE-BANNED */ -} - - -/* - * get_env_copy_with_fallback copies the environment variable with "name" into - * the result buffer. It returns false when it fails. If the environment - * variable is not set the fallback string will be written in the buffer. - * Except when fallback is NULL, in that case an error is returned. - */ -bool -get_env_copy_with_fallback(const char *name, char *result, int maxLength, - const char *fallback) -{ - if (name == NULL || strlen(name) == 0) - { - log_error("Failed to get environment setting. " - "NULL or empty variable name is provided"); - return false; - } - - if (result == NULL) - { - log_error("Failed to get environment setting. " - "Tried to store in NULL pointer"); - return false; - } - - /* - * Explanation of IGNORE-BANNED - * getenv is safe here because we never provide null argument, - * and copy out the result immediately. - */ - const char *envvalue = getenv(name); /* IGNORE-BANNED */ - if (envvalue == NULL) - { - envvalue = fallback; - if (envvalue == NULL) - { - log_error("Failed to get value for environment variable '%s', " - "which is unset", name); - return false; - } - } - - size_t actualLength = strlcpy(result, envvalue, maxLength); - - /* uses >= to make sure the nullbyte fits */ - if (actualLength >= maxLength) - { - log_error("Failed to copy value stored in %s environment setting, " - "which is %lu long. pg_autoctl only supports %lu bytes for " - "this environment setting", - name, - (unsigned long) actualLength, - (unsigned long) maxLength - 1); - return false; - } - return true; -} - - -/* - * get_env_copy copies the environmennt variable with "name" into tho result - * buffer. It returns false when it fails. The environment variable not - * existing is also considered a failure. - */ -bool -get_env_copy(const char *name, char *result, int maxLength) -{ - return get_env_copy_with_fallback(name, result, maxLength, NULL); -} - - -/* - * get_env_pgdata checks for environment value PGDATA - * and copy its value into provided buffer. - * - * function returns true on successful run. returns false - * if it can't find PGDATA or its value is larger than - * the provided buffer - */ -bool -get_env_pgdata(char *pgdata) -{ - return get_env_copy("PGDATA", pgdata, MAXPGPATH) > 0; -} - - -/* - * get_env_pgdata_or_exit does the same as get_env_pgdata. Instead of - * returning false in case of error it exits the process and shows a FATAL log - * message. - */ -void -get_env_pgdata_or_exit(char *pgdata) -{ - if (get_env_pgdata(pgdata)) - { - return; - } - log_fatal("Failed to set PGDATA either from the environment " - "or from --pgdata"); - exit(EXIT_CODE_BAD_ARGS); -} diff --git a/src/bin/pg_autoctl/env_utils.h b/src/bin/pg_autoctl/env_utils.h deleted file mode 100644 index 6dee908ac..000000000 --- a/src/bin/pg_autoctl/env_utils.h +++ /dev/null @@ -1,22 +0,0 @@ -/* - * src/bin/pg_autoctl/env_utils.h - * Utility functions for interacting with environment settings. - * - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the PostgreSQL License. - * - */ - -#ifndef ENV_UTILS_H -#define ENV_UTILS_H - -#include "postgres_fe.h" - -bool env_found_empty(const char *name); -bool env_exists(const char *name); -bool get_env_copy(const char *name, char *outbuffer, int maxLength); -bool get_env_copy_with_fallback(const char *name, char *result, int maxLength, - const char *fallback); -bool get_env_pgdata(char *pgdata); -void get_env_pgdata_or_exit(char *pgdata); -#endif /* ENV_UTILS_H */ diff --git a/src/bin/pg_autoctl/file_utils.c b/src/bin/pg_autoctl/file_utils.c deleted file mode 100644 index 70527168f..000000000 --- a/src/bin/pg_autoctl/file_utils.c +++ /dev/null @@ -1,943 +0,0 @@ -/* - * src/bin/pg_autoctl/file_utils.c - * Implementations of utility functions for reading and writing files - * - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the PostgreSQL License. - * - */ - -#include -#include -#include - -#if defined(__APPLE__) -#include -#endif - -#include "postgres_fe.h" - -#include "snprintf.h" - -#include "cli_root.h" -#include "defaults.h" -#include "env_utils.h" -#include "file_utils.h" -#include "log.h" - -static bool read_file_internal(FILE *fileStream, - const char *filePath, - char **contents, - long *fileSize); - -/* - * file_exists returns true if the given filename is known to exist - * on the file system or false if it does not exist or in case of - * error. - */ -bool -file_exists(const char *filename) -{ - bool exists = access(filename, F_OK) != -1; - if (!exists && errno != 0) - { - /* - * Only log "interesting" errors here. - * - * The fact that the file does not exist is not interesting: we're - * retuning false and the caller figures it out, maybe then creating - * the file. - */ - if (errno != ENOENT && errno != ENOTDIR) - { - log_error("Failed to check if file \"%s\" exists: %m", filename); - } - return false; - } - - return exists; -} - - -/* - * directory_exists returns whether the given path is the name of a directory that - * exists on the file system or not. - */ -bool -directory_exists(const char *path) -{ - struct stat info; - - if (!file_exists(path)) - { - return false; - } - - if (stat(path, &info) != 0) - { - log_error("Failed to stat \"%s\": %m\n", path); - return false; - } - - bool result = (info.st_mode & S_IFMT) == S_IFDIR; - return result; -} - - -/* - * ensure_empty_dir ensures that the given path points to an empty directory with - * the given mode. If it fails to do so, it returns false. - */ -bool -ensure_empty_dir(const char *dirname, int mode) -{ - /* pg_mkdir_p might modify its input, so create a copy of dirname. */ - char dirname_copy[MAXPGPATH]; - strlcpy(dirname_copy, dirname, MAXPGPATH); - - if (directory_exists(dirname)) - { - if (!rmtree(dirname, true)) - { - log_error("Failed to remove directory \"%s\": %m", dirname); - return false; - } - } - else - { - /* - * reset errno, we don't care anymore that it failed because dirname - * doesn't exists. - */ - errno = 0; - } - - if (pg_mkdir_p(dirname_copy, mode) == -1) - { - log_error("Failed to ensure empty directory \"%s\": %m", dirname); - return false; - } - - return true; -} - - -/* - * fopen_with_umask is a version of fopen that gives more control. The main - * advantage of it is that it allows specifying a umask of the file. This makes - * sure files are not accidentally created with umask 777 if the user has it - * configured in a weird way. - * - * This function returns NULL when opening the file fails. So this should be - * handled. It will log an error in this case though, so that's not necessary - * at the callsite. - */ -FILE * -fopen_with_umask(const char *filePath, const char *modes, int flags, mode_t umask) -{ - int fileDescriptor = open(filePath, flags, umask); - if (fileDescriptor == -1) - { - log_error("Failed to open file \"%s\": %m", filePath); - return NULL; - } - - FILE *fileStream = fdopen(fileDescriptor, modes); - if (fileStream == NULL) - { - log_error("Failed to open file \"%s\": %m", filePath); - close(fileDescriptor); - } - return fileStream; -} - - -/* - * fopen_read_only opens the file as a read only stream. - */ -FILE * -fopen_read_only(const char *filePath) -{ - /* - * Explanation of IGNORE-BANNED - * fopen is safe here because we open the file in read only mode. So no - * exclusive access is needed. - */ - return fopen(filePath, "rb"); /* IGNORE-BANNED */ -} - - -/* - * write_file writes the given data to the file given by filePath using - * our logging library to report errors. If succesful, the function returns - * true. - */ -bool -write_file(char *data, long fileSize, const char *filePath) -{ - FILE *fileStream = fopen_with_umask(filePath, "wb", FOPEN_FLAGS_W, 0644); - - if (fileStream == NULL) - { - /* errors have already been logged */ - return false; - } - - if (fwrite(data, sizeof(char), fileSize, fileStream) < fileSize) - { - log_error("Failed to write file \"%s\": %m", filePath); - fclose(fileStream); - return false; - } - - if (fclose(fileStream) == EOF) - { - log_error("Failed to write file \"%s\"", filePath); - return false; - } - - return true; -} - - -/* - * append_to_file writes the given data to the end of the file given by - * filePath using our logging library to report errors. If succesful, the - * function returns true. - */ -bool -append_to_file(char *data, long fileSize, const char *filePath) -{ - FILE *fileStream = fopen_with_umask(filePath, "ab", FOPEN_FLAGS_A, 0644); - - if (fileStream == NULL) - { - /* errors have already been logged */ - return false; - } - - if (fwrite(data, sizeof(char), fileSize, fileStream) < fileSize) - { - log_error("Failed to write file \"%s\": %m", filePath); - fclose(fileStream); - return false; - } - - if (fclose(fileStream) == EOF) - { - log_error("Failed to write file \"%s\"", filePath); - return false; - } - - return true; -} - - -/* - * read_file_if_exists is a utility function that reads the contents of a file - * using our logging library to report errors. ENOENT is not considered worth - * of a log message in this function, and we still return false in that case. - * - * If successful, the function returns true and fileSize points to the number - * of bytes that were read and contents points to a buffer containing the entire - * contents of the file. This buffer should be freed by the caller. - */ -bool -read_file_if_exists(const char *filePath, char **contents, long *fileSize) -{ - /* open a file */ - FILE *fileStream = fopen_read_only(filePath); - - if (fileStream == NULL) - { - if (errno != ENOENT) - { - log_error("Failed to open file \"%s\": %m", filePath); - } - return false; - } - - return read_file_internal(fileStream, filePath, contents, fileSize); -} - - -/* - * read_file is a utility function that reads the contents of a file using our - * logging library to report errors. - * - * If successful, the function returns true and fileSize points to the number - * of bytes that were read and contents points to a buffer containing the entire - * contents of the file. This buffer should be freed by the caller. - */ -bool -read_file(const char *filePath, char **contents, long *fileSize) -{ - /* open a file */ - FILE *fileStream = fopen_read_only(filePath); - if (fileStream == NULL) - { - log_error("Failed to open file \"%s\": %m", filePath); - return false; - } - - return read_file_internal(fileStream, filePath, contents, fileSize); -} - - -/* - * read_file_internal is shared by both read_file and read_file_if_exists - * functions. - */ -static bool -read_file_internal(FILE *fileStream, - const char *filePath, char **contents, long *fileSize) -{ - /* get the file size */ - if (fseek(fileStream, 0, SEEK_END) != 0) - { - log_error("Failed to read file \"%s\": %m", filePath); - fclose(fileStream); - return false; - } - - *fileSize = ftell(fileStream); - if (*fileSize < 0) - { - log_error("Failed to read file \"%s\": %m", filePath); - fclose(fileStream); - return false; - } - - if (fseek(fileStream, 0, SEEK_SET) != 0) - { - log_error("Failed to read file \"%s\": %m", filePath); - fclose(fileStream); - return false; - } - - /* read the contents */ - char *data = malloc(*fileSize + 1); - if (data == NULL) - { - log_error("Failed to allocate %ld bytes", *fileSize); - log_error(ALLOCATION_FAILED_ERROR); - fclose(fileStream); - return false; - } - - if (fread(data, sizeof(char), *fileSize, fileStream) < *fileSize) - { - log_error("Failed to read file \"%s\": %m", filePath); - fclose(fileStream); - free(data); - return false; - } - - if (fclose(fileStream) == EOF) - { - log_error("Failed to read file \"%s\"", filePath); - free(data); - return false; - } - - data[*fileSize] = '\0'; - *contents = data; - - return true; -} - - -/* - * move_file is a utility function to move a file from sourcePath to - * destinationPath. It behaves like mv system command. First attempts to move - * a file using rename. if it fails with EXDEV error, the function duplicates - * the source file with owner and permission information and removes it. - */ -bool -move_file(char *sourcePath, char *destinationPath) -{ - if (strncmp(sourcePath, destinationPath, MAXPGPATH) == 0) - { - /* nothing to do */ - log_warn("Source and destination are the same \"%s\", nothing to move.", - sourcePath); - return true; - } - - if (!file_exists(sourcePath)) - { - log_error("Failed to move file, source file \"%s\" does not exist.", - sourcePath); - return false; - } - - if (file_exists(destinationPath)) - { - log_error("Failed to move file, destination file \"%s\" already exists.", - destinationPath); - return false; - } - - /* first try atomic move operation */ - if (rename(sourcePath, destinationPath) == 0) - { - return true; - } - - /* - * rename fails with errno = EXDEV when moving file to a different file - * system. - */ - if (errno != EXDEV) - { - log_error("Failed to move file \"%s\" to \"%s\": %m", - sourcePath, destinationPath); - return false; - } - - if (!duplicate_file(sourcePath, destinationPath)) - { - /* specific error is already logged */ - log_error("Canceling file move due to errors."); - return false; - } - - /* everything is successful we can remove the file */ - unlink_file(sourcePath); - - return true; -} - - -/* - * duplicate_file is a utility function to duplicate a file from sourcePath to - * destinationPath. It reads the contents of the source file and writes to the - * destination file. It expects non-existing destination file and does not - * copy over if it exists. The function returns true on successful execution. - * - * Note: the function reads the whole file into memory before copying out. - */ -bool -duplicate_file(char *sourcePath, char *destinationPath) -{ - char *fileContents; - long fileSize; - struct stat sourceFileStat; - - if (!read_file(sourcePath, &fileContents, &fileSize)) - { - /* errors are logged */ - return false; - } - - if (file_exists(destinationPath)) - { - log_error("Failed to duplicate, destination file already exists : %s", - destinationPath); - return false; - } - - bool foundError = !write_file(fileContents, fileSize, destinationPath); - - free(fileContents); - - if (foundError) - { - /* errors are logged in write_file */ - return false; - } - - /* set uid gid and mode */ - if (stat(sourcePath, &sourceFileStat) != 0) - { - log_error("Failed to get ownership and file permissions on \"%s\"", - sourcePath); - foundError = true; - } - else - { - if (chown(destinationPath, sourceFileStat.st_uid, sourceFileStat.st_gid) != 0) - { - log_error("Failed to set user and group id on \"%s\"", - destinationPath); - foundError = true; - } - if (chmod(destinationPath, sourceFileStat.st_mode) != 0) - { - log_error("Failed to set file permissions on \"%s\"", - destinationPath); - foundError = true; - } - } - - if (foundError) - { - /* errors are already logged */ - unlink_file(destinationPath); - return false; - } - - return true; -} - - -/* - * create_symbolic_link creates a symbolic link to source path. - */ -bool -create_symbolic_link(char *sourcePath, char *targetPath) -{ - if (symlink(sourcePath, targetPath) != 0) - { - log_error("Failed to create symbolic link to \"%s\": %m", targetPath); - return false; - } - return true; -} - - -/* - * path_in_same_directory constructs the path for a file with name fileName - * that is in the same directory as basePath, which should be an absolute - * path. The result is written to destinationPath, which should be at least - * MAXPATH in size. - */ -void -path_in_same_directory(const char *basePath, const char *fileName, - char *destinationPath) -{ - strlcpy(destinationPath, basePath, MAXPGPATH); - get_parent_directory(destinationPath); - join_path_components(destinationPath, destinationPath, fileName); -} - - -/* From PostgreSQL sources at src/port/path.c */ -#ifndef WIN32 -#define IS_PATH_VAR_SEP(ch) ((ch) == ':') -#else -#define IS_PATH_VAR_SEP(ch) ((ch) == ';') -#endif - - -/* - * search_path_first copies the first entry found in PATH to result. result - * should be a buffer of (at least) MAXPGPATH size. - * The function returns false and logs an error when it cannot find the command - * in PATH. - */ -bool -search_path_first(const char *filename, char *result, int logLevel) -{ - SearchPath paths = { 0 }; - - if (!search_path(filename, &paths) || paths.found == 0) - { - log_level(logLevel, "Failed to find %s command in your PATH", filename); - return false; - } - - strlcpy(result, paths.matches[0], MAXPGPATH); - - return true; -} - - -/* - * Searches all the directories in the PATH environment variable for the given - * filename. Returns number of occurrences and each match found with its - * fullname, including the given filename, in the given pre-allocated - * SearchPath result. - */ -bool -search_path(const char *filename, SearchPath *result) -{ - char pathlist[MAXPATHSIZE] = { 0 }; - - /* we didn't count nor find anything yet */ - result->found = 0; - - /* Create a copy of pathlist, because we modify it here. */ - if (!get_env_copy("PATH", pathlist, sizeof(pathlist))) - { - /* errors have already been logged */ - return false; - } - - char *path = pathlist; - - while (path != NULL) - { - char candidate[MAXPGPATH] = { 0 }; - char *sep = first_path_var_separator(path); - - /* split path on current token, null-terminating string at separator */ - if (sep != NULL) - { - *sep = '\0'; - } - - (void) join_path_components(candidate, path, filename); - (void) canonicalize_path(candidate); - - if (file_exists(candidate)) - { - strlcpy(result->matches[result->found++], candidate, MAXPGPATH); - } - - path = (sep == NULL ? NULL : sep + 1); - } - - return true; -} - - -/* - * search_path_deduplicate_symlinks traverse the SearchPath result obtained by - * calling the search_path() function and removes entries that are pointing to - * the same binary on-disk. - * - * In modern debian installations, for instance, we have /bin -> /usr/bin; and - * then we might find pg_config both in /bin/pg_config and /usr/bin/pg_config - * although it's only been installed once, and both are the same file. - * - * We use realpath() to deduplicate entries, and keep the entry that is not a - * symbolic link. - */ -bool -search_path_deduplicate_symlinks(SearchPath *results, SearchPath *dedup) -{ - /* now re-initialize the target structure dedup */ - dedup->found = 0; - - for (int rIndex = 0; rIndex < results->found; rIndex++) - { - bool alreadyThere = false; - - char *currentPath = results->matches[rIndex]; - char currentRealPath[PATH_MAX] = { 0 }; - - if (realpath(currentPath, currentRealPath) == NULL) - { - log_error("Failed to normalize file name \"%s\": %m", currentPath); - return false; - } - - /* add-in the realpath to dedup, unless it's already in there */ - for (int dIndex = 0; dIndex < dedup->found; dIndex++) - { - if (strcmp(dedup->matches[dIndex], currentRealPath) == 0) - { - alreadyThere = true; - - log_debug("dedup: skipping \"%s\"", currentPath); - break; - } - } - - if (!alreadyThere) - { - int bytesWritten = - strlcpy(dedup->matches[dedup->found++], - currentRealPath, - MAXPGPATH); - - if (bytesWritten >= MAXPGPATH) - { - log_error( - "Real path \"%s\" is %d bytes long, and pg_autoctl " - "is limited to handling paths of %d bytes long, maximum", - currentRealPath, - (int) strlen(currentRealPath), - MAXPGPATH); - - return false; - } - } - } - - return true; -} - - -/* - * unlink_state_file calls unlink(2) on the state file to make sure we don't - * leave a lingering state on-disk. - */ -bool -unlink_file(const char *filename) -{ - if (unlink(filename) == -1) - { - /* if it didn't exist yet, good news! */ - if (errno != ENOENT && errno != ENOTDIR) - { - log_error("Failed to remove file \"%s\": %m", filename); - return false; - } - } - - return true; -} - - -/* - * get_program_absolute_path returns the absolute path of the current program - * being executed. Note: the shell is responsible to set that in interactive - * environments, and when the pg_autoctl binary is in the PATH of the user, - * then argv[0] (here pg_autoctl_argv0) is just "pg_autoctl". - */ -bool -set_program_absolute_path(char *program, int size) -{ -#if defined(__APPLE__) - int actualSize = _NSGetExecutablePath(program, (uint32_t *) &size); - - if (actualSize != 0) - { - log_error("Failed to get absolute path for the pg_autoctl program, " - "absolute path requires %d bytes and we support paths up " - "to %d bytes only", actualSize, size); - return false; - } - - log_debug("Found absolute program: \"%s\"", program); - -#else - - /* - * On Linux and FreeBSD and Solaris, we can find a symbolic link to our - * program and get the information with readlink. Of course the /proc entry - * to read is not the same on both systems, so we try several things here. - */ - bool found = false; - char *procEntryCandidates[] = { - "/proc/self/exe", /* Linux */ - "/proc/curproc/file", /* FreeBSD */ - "/proc/self/path/a.out" /* Solaris */ - }; - int procEntrySize = sizeof(procEntryCandidates) / sizeof(char *); - int procEntryIndex = 0; - - for (procEntryIndex = 0; procEntryIndex < procEntrySize; procEntryIndex++) - { - if (readlink(procEntryCandidates[procEntryIndex], program, size) != -1) - { - found = true; - log_debug("Found absolute program \"%s\" in \"%s\"", - program, - procEntryCandidates[procEntryIndex]); - } - else - { - /* when the file does not exist, we try our next guess */ - if (errno != ENOENT && errno != ENOTDIR) - { - log_error("Failed to get absolute path for the " - "pg_autoctl program: %m"); - return false; - } - } - } - - if (found) - { - return true; - } - else - { - /* - * Now either return pg_autoctl_argv0 when that's an absolute filename, - * or search for it in the PATH otherwise. - */ - SearchPath paths = { 0 }; - - if (pg_autoctl_argv0[0] == '/') - { - strlcpy(program, pg_autoctl_argv0, size); - return true; - } - - if (!search_path(pg_autoctl_argv0, &paths) || paths.found == 0) - { - log_error("Failed to find \"%s\" in PATH environment", - pg_autoctl_argv0); - exit(EXIT_CODE_INTERNAL_ERROR); - } - else - { - log_debug("Found \"%s\" in PATH at \"%s\"", - pg_autoctl_argv0, paths.matches[0]); - strlcpy(program, paths.matches[0], size); - - return true; - } - } -#endif - - return true; -} - - -/* - * normalize_filename returns the real path of a given filename that belongs to - * an existing file on-disk, resolving symlinks and pruning double-slashes and - * other weird constructs. filename and dst are allowed to point to the same - * adress. - */ -bool -normalize_filename(const char *filename, char *dst, int size) -{ - /* normalize the path to the configuration file, if it exists */ - if (file_exists(filename)) - { - char realPath[PATH_MAX] = { 0 }; - - if (realpath(filename, realPath) == NULL) - { - log_fatal("Failed to normalize file name \"%s\": %m", filename); - return false; - } - - if (strlcpy(dst, realPath, size) >= size) - { - log_fatal("Real path \"%s\" is %d bytes long, and pg_autoctl " - "is limited to handling paths of %d bytes long, maximum", - realPath, (int) strlen(realPath), size); - return false; - } - } - else - { - char realPath[PATH_MAX] = { 0 }; - - /* protect against undefined behavior if dst overlaps with filename */ - strlcpy(realPath, filename, MAXPGPATH); - strlcpy(dst, realPath, MAXPGPATH); - } - - return true; -} - - -/* - * fformat is a secured down version of pg_fprintf: - * - * Additional security checks are: - * - make sure stream is not null - * - make sure fmt is not null - * - rely on pg_fprintf Assert() that %s arguments are not null - */ -int -fformat(FILE *stream, const char *fmt, ...) -{ - va_list args; - - if (stream == NULL || fmt == NULL) - { - log_error("BUG: fformat is called with a NULL target or format string"); - return -1; - } - - va_start(args, fmt); - int len = pg_vfprintf(stream, fmt, args); - va_end(args); - return len; -} - - -/* - * sformat is a secured down version of pg_snprintf - */ -int -sformat(char *str, size_t count, const char *fmt, ...) -{ - va_list args; - - if (str == NULL || fmt == NULL) - { - log_error("BUG: sformat is called with a NULL target or format string"); - return -1; - } - - va_start(args, fmt); - int len = pg_vsnprintf(str, count, fmt, args); - va_end(args); - - if (len >= count) - { - log_error("BUG: sformat needs %d bytes to expend format string \"%s\", " - "and a target string of %lu bytes only has been given.", - len, fmt, - (unsigned long) count); - } - - return len; -} - - -/* - * set_ps_title sets the process title seen in ps/top and friends, truncating - * if there is not enough space, rather than causing memory corruption. - * - * Inspired / stolen from Postgres code src/backend/utils/misc/ps_status.c with - * most of the portability bits removed. At the moment we prefer simple code - * that works on few targets to highly portable code. - */ -void -init_ps_buffer(int argc, char **argv) -{ -#if defined(__linux__) || defined(__darwin__) - char *end_of_area = NULL; - int i; - - /* - * check for contiguous argv strings - */ - for (i = 0; i < argc; i++) - { - if (i == 0 || end_of_area + 1 == argv[i]) - { - end_of_area = argv[i] + strlen(argv[i]); /* lgtm[cpp/tainted-arithmetic] */ - } - } - - if (end_of_area == NULL) /* probably can't happen? */ - { - ps_buffer = NULL; - ps_buffer_size = 0; - return; - } - - ps_buffer = argv[0]; - last_status_len = ps_buffer_size = end_of_area - argv[0]; /* lgtm[cpp/tainted-arithmetic] */ - -#else - ps_buffer = NULL; - ps_buffer_size = 0; - - return; -#endif -} - - -/* - * set_ps_title sets our process name visible in ps/top/pstree etc. - */ -void -set_ps_title(const char *title) -{ - if (ps_buffer == NULL) - { - /* noop */ - return; - } - - int n = sformat(ps_buffer, ps_buffer_size, "%s", title); - - /* pad our process title string */ - for (size_t i = n; i < ps_buffer_size; i++) - { - *(ps_buffer + i) = '\0'; - } -} diff --git a/src/bin/pg_autoctl/file_utils.h b/src/bin/pg_autoctl/file_utils.h deleted file mode 100644 index 98745f4dc..000000000 --- a/src/bin/pg_autoctl/file_utils.h +++ /dev/null @@ -1,73 +0,0 @@ -/* - * src/bin/pg_autoctl/file_utils.h - * Utility functions for reading and writing files - * - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the PostgreSQL License. - * - */ - -#ifndef FILE_UTILS_H -#define FILE_UTILS_H - -#include - -#include "postgres_fe.h" - -#include - - -#if defined(__APPLE__) -#define ST_MTIME_S(st) ((int64_t) st.st_mtimespec.tv_sec) -#else -#define ST_MTIME_S(st) ((int64_t) st.st_mtime) -#endif - - -/* - * In order to avoid dynamic memory allocations and tracking when searching the - * PATH environment, we pre-allocate 1024 paths entries. That should be way - * more than enough for all situations, and only costs 1024*1024 = 1MB of - * memory. - */ -typedef struct SearchPath -{ - int found; - char matches[1024][MAXPGPATH]; -} SearchPath; - - -bool file_exists(const char *filename); -bool directory_exists(const char *path); -bool ensure_empty_dir(const char *dirname, int mode); -FILE * fopen_with_umask(const char *filePath, const char *modes, int flags, mode_t umask); -FILE * fopen_read_only(const char *filePath); -bool write_file(char *data, long fileSize, const char *filePath); -bool append_to_file(char *data, long fileSize, const char *filePath); -bool read_file(const char *filePath, char **contents, long *fileSize); -bool read_file_if_exists(const char *filePath, char **contents, long *fileSize); -bool move_file(char *sourcePath, char *destinationPath); -bool duplicate_file(char *sourcePath, char *destinationPath); -bool create_symbolic_link(char *sourcePath, char *targetPath); - -void path_in_same_directory(const char *basePath, - const char *fileName, - char *destinationPath); - -bool search_path_first(const char *filename, char *result, int logLevel); -bool search_path(const char *filename, SearchPath *result); -bool search_path_deduplicate_symlinks(SearchPath *results, SearchPath *dedup); -bool unlink_file(const char *filename); -bool set_program_absolute_path(char *program, int size); -bool normalize_filename(const char *filename, char *dst, int size); - -void init_ps_buffer(int argc, char **argv); -void set_ps_title(const char *title); - -int fformat(FILE *stream, const char *fmt, ...) -__attribute__((format(printf, 2, 3))); - -int sformat(char *str, size_t count, const char *fmt, ...) -__attribute__((format(printf, 3, 4))); - -#endif /* FILE_UTILS_H */ diff --git a/src/bin/pg_autoctl/ini_file.c b/src/bin/pg_autoctl/ini_file.c deleted file mode 100644 index 40c88016e..000000000 --- a/src/bin/pg_autoctl/ini_file.c +++ /dev/null @@ -1,740 +0,0 @@ -/* - * src/bin/pg_autoctl/ini_file.c - * Functions to parse a configuration file using the .INI syntax. - * - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the PostgreSQL License. - * - */ - -#include -#include - -#include "ini.h" -#include "ini_file.h" -#include "log.h" -#include "pgctl.h" -#include "parson.h" -#include "pgsetup.h" -#include "string_utils.h" - -/* - * Load a configuration file in the INI format. - */ -bool -read_ini_file(const char *filename, IniOption *optionList) -{ - char *fileContents = NULL; - long fileSize = 0L; - - /* read the current postgresql.conf contents */ - if (!read_file(filename, &fileContents, &fileSize)) - { - return false; - } - - return parse_ini_buffer(filename, fileContents, optionList); -} - - -/* - * parse_ini_buffer parses the content of a config.ini file. - */ -bool -parse_ini_buffer(const char *filename, - char *fileContents, - IniOption *optionList) -{ - IniOption *option; - - /* parse the content of the file as per INI syntax rules */ - ini_t *ini = ini_load(fileContents, NULL); - free(fileContents); - - /* - * Now that the INI file is loaded into a generic structure, run through it - * to find given opts and set. - */ - for (option = optionList; option->type != INI_END_T; option++) - { - int optionIndex; - char *val; - - int sectionIndex = ini_find_section(ini, option->section, 0); - - if (sectionIndex == INI_NOT_FOUND) - { - if (option->required) - { - log_error("Failed to find section %s in \"%s\"", - option->section, filename); - ini_destroy(ini); - return false; - } - optionIndex = INI_NOT_FOUND; - } - else - { - optionIndex = ini_find_property(ini, sectionIndex, option->name, 0); - } - - /* - * When we didn't find an option, we have three cases to consider: - * 1. it's required, error out - * 2. it's a compatibility option, skip it - * 3. use the default value instead - */ - if (optionIndex == INI_NOT_FOUND) - { - if (option->required) - { - log_error("Failed to find option %s.%s in \"%s\"", - option->section, option->name, filename); - ini_destroy(ini); - return false; - } - else if (option->compat) - { - /* skip compatibility options that are not found */ - continue; - } - else - { - switch (option->type) - { - case INI_INT_T: - { - *(option->intValue) = option->intDefault; - break; - } - - case INI_STRING_T: - case INI_STRBUF_T: - { - ini_set_option_value(option, option->strDefault); - break; - } - - default: - - /* should never happen, or it's a development bug */ - log_fatal("Unknown option type %d", option->type); - ini_destroy(ini); - return false; - } - } - } - else - { - val = (char *) ini_property_value(ini, sectionIndex, optionIndex); - - log_trace("%s.%s = %s", option->section, option->name, val); - - if (val != NULL) - { - if (!ini_set_option_value(option, val)) - { - /* we logged about it already */ - ini_destroy(ini); - return false; - } - } - } - } - ini_destroy(ini); - return true; -} - - -/* - * ini_validate_options walks through an optionList and installs default values - * when necessary, and returns false if any required option is missing and - * doesn't have a default provided. - */ -bool -ini_validate_options(IniOption *optionList) -{ - IniOption *option; - - for (option = optionList; option->type != INI_END_T; option++) - { - char optionName[BUFSIZE]; - - int n = sformat(optionName, BUFSIZE, "%s.%s", option->section, option->name); - - if (option->optName) - { - sformat(optionName + n, BUFSIZE - n, " (--%s)", option->optName); - } - - switch (option->type) - { - case INI_INT_T: - { - if (*(option->intValue) == -1 && option->intDefault != -1) - { - *(option->intValue) = option->intDefault; - } - - if (option->required && *(option->intValue) == -1) - { - log_error("Option %s is required and has not been set", - optionName); - return false; - } - break; - } - - case INI_STRING_T: - { - if (*(option->strValue) == NULL && option->strDefault != NULL) - { - ini_set_option_value(option, option->strDefault); - } - - if (option->required && *(option->strValue) == NULL) - { - log_error("Option %ss is required and has not been set", - optionName); - return false; - } - break; - } - - case INI_STRBUF_T: - { - if (IS_EMPTY_STRING_BUFFER(option->strBufValue) && - option->strDefault != NULL) - { - ini_set_option_value(option, option->strDefault); - } - - if (option->required && IS_EMPTY_STRING_BUFFER(option->strBufValue)) - { - log_error("Option %s is required and has not been set", - optionName); - return false; - } - break; - } - - default: - - /* should never happen, or it's a development bug */ - log_fatal("Unknown option type %d", option->type); - return false; - } - } - return true; -} - - -/* - * ini_set_option_value saves given value to option, parsing the value string - * as its type require. - */ -bool -ini_set_option_value(IniOption *option, const char *value) -{ - if (option == NULL) - { - return false; - } - - switch (option->type) - { - case INI_STRING_T: - { - if (value == NULL) - { - *(option->strValue) = NULL; - } - else - { - *(option->strValue) = strdup(value); - if (*(option->strValue) == NULL) - { - log_error(ALLOCATION_FAILED_ERROR); - return false; - } - } - break; - } - - case INI_STRBUF_T: - { - /* - * When given a String Buffer str[SIZE], then we are given strbuf - * as the address where to host the data directly. - */ - if (value == NULL) - { - /* null are handled as bytes of '\0' in string buffers */ - bzero((void *) option->strBufValue, option->strBufferSize); - } - else - { - strlcpy((char *) option->strBufValue, value, option->strBufferSize); - } - break; - } - - case INI_INT_T: - { - if (value) - { - int nb; - - if (!stringToInt(value, &nb)) - { - log_error("Failed to parse %s.%s's value \"%s\" as a number", - option->section, option->name, value); - return false; - } - *(option->intValue) = nb; - } - break; - } - - default: - { - /* developer error, should never happen */ - log_fatal("Unknown option type %d", option->type); - return false; - } - } - return true; -} - - -/* - * Format a single option as a string value. - */ -bool -ini_option_to_string(IniOption *option, char *dest, size_t size) -{ - switch (option->type) - { - case INI_STRING_T: - { - /* option->strValue is a char **, both pointers could be NULL */ - if (option->strValue == NULL || *(option->strValue) == NULL) - { - return false; - } - - strlcpy(dest, *(option->strValue), size); - return true; - } - - case INI_STRBUF_T: - { - strlcpy(dest, (char *) option->strBufValue, size); - return true; - } - - case INI_INT_T: - { - sformat(dest, size, "%d", *(option->intValue)); - return true; - } - - default: - { - log_fatal("Unknown option type %d", option->type); - return false; - } - } - return false; -} - - -#define streq(x, y) ((x != NULL) && (y != NULL) && ( \ - strcmp(x, y) == 0)) - -/* - * write_ini_to_stream writes in-memory INI structure to given STREAM in the - * INI format specifications. - */ -bool -write_ini_to_stream(FILE *stream, IniOption *optionList) -{ - char *currentSection = NULL; - IniOption *option; - - for (option = optionList; option->type != INI_END_T; option++) - { - /* we read "compatibility" options but never write them back */ - if (option->compat) - { - continue; - } - - /* we might need to open a new section */ - if (!streq(currentSection, option->section)) - { - if (currentSection != NULL) - { - fformat(stream, "\n"); - } - currentSection = (char *) option->section; - fformat(stream, "[%s]\n", currentSection); - } - - switch (option->type) - { - case INI_INT_T: - { - fformat(stream, "%s = %d\n", - option->name, *(option->intValue)); - break; - } - - case INI_STRING_T: - { - char *value = *(option->strValue); - - if (value) - { - fformat(stream, "%s = %s\n", option->name, value); - } - else if (option->required) - { - log_error("Option %s.%s is required but is not set", - option->section, option->name); - return false; - } - break; - } - - case INI_STRBUF_T: - { - /* here we have a string buffer, which is its own address */ - char *value = (char *) option->strBufValue; - - if (value[0] != '\0') - { - fformat(stream, "%s = %s\n", option->name, value); - } - else if (option->required) - { - log_error("Option %s.%s is required but is not set", - option->section, option->name); - return false; - } - break; - } - - default: - { - /* developper error, should never happen */ - log_fatal("Unknown option type %d", option->type); - break; - } - } - } - fflush(stream); - return true; -} - - -/* - * ini_to_json populates the given JSON value with the contents of the INI - * file. Sections become JSON objects, options the keys to the section objects. - */ -bool -ini_to_json(JSON_Object *jsRoot, IniOption *optionList) -{ - char *currentSection = NULL; - JSON_Value *currentSectionJs = NULL; - JSON_Object *currentSectionJsObj = NULL; - IniOption *option = NULL; - - for (option = optionList; option->type != INI_END_T; option++) - { - /* we read "compatibility" options but never write them back */ - if (option->compat) - { - continue; - } - - /* we might need to open a new section */ - if (!streq(currentSection, option->section)) - { - if (currentSection != NULL) - { - json_object_set_value(jsRoot, currentSection, currentSectionJs); - } - - currentSectionJs = json_value_init_object(); - currentSectionJsObj = json_value_get_object(currentSectionJs); - - currentSection = (char *) option->section; - } - - switch (option->type) - { - case INI_INT_T: - { - json_object_set_number(currentSectionJsObj, - option->name, - (double) *(option->intValue)); - break; - } - - case INI_STRING_T: - { - char *value = *(option->strValue); - - if (value) - { - json_object_set_string(currentSectionJsObj, - option->name, - value); - } - else if (option->required) - { - log_error("Option %s.%s is required but is not set", - option->section, option->name); - return false; - } - break; - } - - case INI_STRBUF_T: - { - /* here we have a string buffer, which is its own address */ - char *value = (char *) option->strBufValue; - - if (value[0] != '\0') - { - json_object_set_string(currentSectionJsObj, - option->name, - value); - } - else if (option->required) - { - log_error("Option %s.%s is required but is not set", - option->section, option->name); - return false; - } - break; - } - - default: - { - /* developper error, should never happen */ - log_fatal("Unknown option type %d", option->type); - break; - } - } - } - - if (currentSection != NULL) - { - json_object_set_value(jsRoot, currentSection, currentSectionJs); - } - - return true; -} - - -/* - * lookup_ini_option implements an option lookup given a section name and an - * option name. - */ -IniOption * -lookup_ini_option(IniOption *optionList, const char *section, const char *name) -{ - IniOption *option; - - /* now lookup section/option names in opts */ - for (option = optionList; option->type != INI_END_T; option++) - { - if (streq(option->section, section) && streq(option->name, name)) - { - return option; - } - } - return NULL; -} - - -/* - * Lookup an option value given a "path" of section.option. - */ -IniOption * -lookup_ini_path_value(IniOption *optionList, const char *path) -{ - char *section_name, *option_name, *ptr; - - /* - * Split path into section/option. - */ - ptr = strchr(path, '.'); - - if (ptr == NULL) - { - log_error("Failed to find a dot separator in option path \"%s\"", path); - return NULL; - } - - section_name = strdup(path); /* don't scribble on path */ - if (section_name == NULL) - { - log_error(ALLOCATION_FAILED_ERROR); - return NULL; - } - option_name = section_name + (ptr - path) + 1; /* apply same offset */ - *(option_name - 1) = '\0'; /* split string at the dot */ - - IniOption *option = lookup_ini_option(optionList, section_name, option_name); - - if (option == NULL) - { - log_error("Failed to find configuration option for path \"%s\"", path); - } - - free(section_name); - - return option; -} - - -/* - * ini_merge merges the options that have been set in overrideOptionList into - * the options in dstOptionList, ignoring default values. - */ -bool -ini_merge(IniOption *dstOptionList, IniOption *overrideOptionList) -{ - IniOption *option; - - for (option = overrideOptionList; option->type != INI_END_T; option++) - { - IniOption *dstOption = - lookup_ini_option(dstOptionList, option->section, option->name); - - if (dstOption == NULL) - { - /* developper error, why do we have incompatible INI options? */ - log_error("BUG: ini_merge: lookup failed in dstOptionList(%s, %s)", - option->section, option->name); - return false; - } - - switch (option->type) - { - case INI_INT_T: - { - if (*(option->intValue) != -1 && *(option->intValue) != 0) - { - *(dstOption->intValue) = *(option->intValue); - } - break; - } - - case INI_STRING_T: - { - if (*(option->strValue) != NULL) - { - *(dstOption->strValue) = strdup(*(option->strValue)); - if (*(dstOption->strValue) == NULL) - { - log_error(ALLOCATION_FAILED_ERROR); - return false; - } - } - break; - } - - case INI_STRBUF_T: - { - if (!IS_EMPTY_STRING_BUFFER(option->strBufValue)) - { - strlcpy((char *) dstOption->strBufValue, - (char *) option->strBufValue, - dstOption->strBufferSize); - } - break; - } - - default: - - /* should never happen, or it's a development bug */ - log_fatal("Unknown option type %d", option->type); - return false; - } - } - return true; -} - - -/* - * ini_get_setting reads given INI filename and maps its content using an - * optionList that instructs which options to read and what default values to - * use. Then ini_get_setting looks up the given path (section.option) and sets - * the given value string. - */ -bool -ini_get_setting(const char *filename, IniOption *optionList, - const char *path, char *value, size_t size) -{ - log_debug("Reading configuration from \"%s\"", filename); - - if (!read_ini_file(filename, optionList)) - { - log_error("Failed to parse configuration file \"%s\"", filename); - return false; - } - - IniOption *option = lookup_ini_path_value(optionList, path); - - if (option) - { - return ini_option_to_string(option, value, size); - } - - return false; -} - - -/* - * ini_set_option sets the INI value to the given value. - */ -bool -ini_set_option(IniOption *optionList, const char *path, char *value) -{ - IniOption *option = lookup_ini_path_value(optionList, path); - - if (option && ini_set_option_value(option, value)) - { - log_debug("ini_set_option %s.%s = %s", - option->section, option->name, value); - - return true; - } - - return false; -} - - -/* - * ini_set_setting sets the INI filename option identified by path to the given - * value. optionList is used to know how to read the values in the file and - * also contains the default values. - */ -bool -ini_set_setting(const char *filename, IniOption *optionList, - const char *path, char *value) -{ - log_debug("Reading configuration from %s", filename); - - if (!read_ini_file(filename, optionList)) - { - log_error("Failed to parse configuration file \"%s\"", filename); - return false; - } - - return ini_set_option(optionList, path, value); -} diff --git a/src/bin/pg_autoctl/ini_file.h b/src/bin/pg_autoctl/ini_file.h deleted file mode 100644 index 7e3d61918..000000000 --- a/src/bin/pg_autoctl/ini_file.h +++ /dev/null @@ -1,117 +0,0 @@ -/* - * src/bin/pg_autoctl/ini_file.h - * Functions to parse a configuration file using the .INI syntax. - * - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the PostgreSQL License. - * - */ - -#ifndef INI_FILE_H -#define INI_FILE_H - -#include -#include - -#include "parson.h" - -#define INI_STRING_T 1 /* char *target */ -#define INI_STRBUF_T 2 /* char target[size] */ -#define INI_INT_T 3 /* int target */ -#define INI_END_T 4 - -/* - * IniOption represent a key/value as written in the INI format: - * - * [section] - * name = "values" - * int = 10 - * - * The IniOption structure is used both for specifying what we expect to read - * in the INI file: required, strdefault, and intdefault, and what has been - * actually read from it: strval/intval. - * - * Given the previous contents and this structure as input: - * - * { - * {INI_STRING_T, "section", "name", true, "default", -1, -1, &str, NULL}, - * {INI_INT_T, "section", "int", true, NULL, 1, -1, NULL, &int}, - * {INI_END_T, NULL, NULL, false, NULL, -1, -1, NULL, NULL} - * } - * - * Then after reading the ini file with `read_ini_file' then *str = "values" - * and *int = 10. - */ -typedef struct IniOption -{ - int type; - const char *section; - const char *name; - const char *optName; /* command line option name */ - bool required; - bool compat; /* compatibility: read but don't write */ - char *strDefault; /* default value when type is string */ - int intDefault; /* default value when type is int */ - int strBufferSize; /* size of the BUFFER when INI_STRBUF_T */ - char **strValue; /* pointer to a string pointer (typically malloc-ed) */ - char *strBufValue; /* pointer to a string buffer (on the stack) */ - int *intValue; /* pointer to an integer */ -} IniOption; - -#define make_int_option(section, name, optName, required, value) \ - { INI_INT_T, section, name, optName, required, false, \ - NULL, -1, -1, NULL, NULL, value } - -#define make_int_option_default(section, name, optName, \ - required, value, default) \ - { INI_INT_T, section, name, optName, required, false, \ - NULL, default, -1, NULL, NULL, value } - -#define make_string_option(section, name, optName, required, value) \ - { INI_STRING_T, section, name, optName, required, false, \ - NULL, -1, -1, value, NULL, NULL } - -#define make_string_option_default(section, name, optName, required, \ - value, default) \ - { INI_STRING_T, section, name, optName, required, false, \ - default, -1, -1, value, NULL, NULL } - -#define make_strbuf_option(section, name, optName, required, size, value) \ - { INI_STRBUF_T, section, name, optName, required, false, \ - NULL, -1, size, NULL, value, NULL } - -#define make_strbuf_compat_option(section, name, size, value) \ - { INI_STRBUF_T, section, name, NULL, false, true, \ - NULL, -1, size, NULL, value, NULL } - -#define make_strbuf_option_default(section, name, optName, required, \ - size, value, default) \ - { INI_STRBUF_T, section, name, optName, required, false, \ - default, -1, size, NULL, value, NULL } - -#define INI_OPTION_LAST \ - { INI_END_T, NULL, NULL, NULL, false, false, NULL, -1, -1, NULL, NULL, NULL } - -bool read_ini_file(const char *filename, IniOption *opts); -bool parse_ini_buffer(const char *filename, - char *fileContents, - IniOption *optionList); -bool ini_validate_options(IniOption *optionList); -bool ini_set_option_value(IniOption *option, const char *value); -bool ini_option_to_string(IniOption *option, char *dest, size_t size); -bool write_ini_to_stream(FILE *stream, IniOption *optionList); -bool ini_to_json(JSON_Object *jsRoot, IniOption *optionList); -IniOption * lookup_ini_option(IniOption *optionList, - const char *section, const char *name); -IniOption * lookup_ini_path_value(IniOption *optionList, const char *path); -bool ini_merge(IniOption *dstOptionList, IniOption *overrideOptionList); - -bool ini_set_option(IniOption *optionList, const char *path, char *value); - -bool ini_get_setting(const char *filename, IniOption *optionList, - const char *path, char *value, size_t size); -bool ini_set_setting(const char *filename, IniOption *optionList, - const char *path, char *value); - - -#endif /* INI_FILE_H */ diff --git a/src/bin/pg_autoctl/ini_implementation.c b/src/bin/pg_autoctl/ini_implementation.c deleted file mode 100644 index 963095207..000000000 --- a/src/bin/pg_autoctl/ini_implementation.c +++ /dev/null @@ -1,13 +0,0 @@ -/* - * src/bin/pg_autoctl/ini_implementation.c - * The file containing library code used to parse files with .INI syntax - * - * The main reason this is in a separate file is so you can exclude a file - * during static analysis. This way we exclude vendored in library code, - * but not our code using it. - * - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the PostgreSQL License. - */ -#define INI_IMPLEMENTATION -#include "ini.h" diff --git a/src/bin/pg_autoctl/ipaddr.c b/src/bin/pg_autoctl/ipaddr.c deleted file mode 100644 index 1ef03f9a7..000000000 --- a/src/bin/pg_autoctl/ipaddr.c +++ /dev/null @@ -1,920 +0,0 @@ -/* - * src/bin/pg_autoctl/ipaddr.c - * Find local ip used as source ip in ip packets, using getsockname and a udp - * connection. - * - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the PostgreSQL License. - * - */ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "postgres_fe.h" - -#include "defaults.h" -#include "env_utils.h" -#include "file_utils.h" -#include "ipaddr.h" -#include "log.h" -#include "pgsetup.h" -#include "pgsql.h" -#include "string_utils.h" - -static unsigned int countSetBits(unsigned int n); -static unsigned int countSetBitsv6(unsigned char *addr); -static bool ipv4eq(struct sockaddr_in *a, struct sockaddr_in *b); -static bool ipv6eq(struct sockaddr_in6 *a, struct sockaddr_in6 *b); -static bool fetchIPAddressFromInterfaceList(char *localIpAddress, int size); -static bool ipaddr_sockaddr_to_string(struct addrinfo *ai, - char *ipaddr, size_t size); -static bool ipaddr_getsockname(int sock, char *ipaddr, size_t size); -static bool GetAddrInfo(const char *restrict node, - const char *restrict service, - const struct addrinfo *restrict hints, - struct addrinfo **restrict res); - - -/* - * Connect to given serviceName and servicePort in TCP in order to determine - * which local IP address has been used to connect. That local IP address is - * then the one we use for the default --hostname value, when not provided. - * - * On a keeper, we use the monitor hostname as the serviceName. On the monitor, - * we use DEFAULT_INTERFACE_LOOKUP_SERVICE_NAME to discover the local default - * outbound IP address. - */ -bool -fetchLocalIPAddress(char *localIpAddress, int size, - const char *serviceName, int servicePort, - int logLevel, bool *mayRetry) -{ - struct addrinfo *lookup; - struct addrinfo *ai; - struct addrinfo hints; - - bool couldConnect = false; - - int sock; - - *mayRetry = false; - - /* prepare getaddrinfo hints for name resolution or IP address parsing */ - memset(&hints, 0, sizeof(hints)); - hints.ai_family = PF_UNSPEC; /* accept any family as supported by OS */ - hints.ai_socktype = SOCK_STREAM; /* we only want TCP sockets */ - hints.ai_protocol = IPPROTO_TCP; /* we only want TCP sockets */ - - if (!GetAddrInfo(serviceName, - intToString(servicePort).strValue, - &hints, - &lookup)) - { - /* errors have already been logged */ - return false; - } - - for (ai = lookup; ai; ai = ai->ai_next) - { - char addr[BUFSIZE] = { 0 }; - - if (!ipaddr_sockaddr_to_string(ai, addr, sizeof(addr))) - { - /* errors have already been logged */ - return false; - } - - sock = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol); - - if (sock < 0) - { - log_warn("Failed to create a socket: %m"); - return false; - } - - /* connect timeout can be quite long by default */ - log_info("Connecting to %s (port %d)", addr, servicePort); - - int err = connect(sock, ai->ai_addr, ai->ai_addrlen); - - if (err < 0) - { - log_level(logLevel, "Failed to connect to %s: %m", addr); - } - else - { - /* found a getaddrinfo() result we could use to connect */ - couldConnect = true; - break; - } - } - - freeaddrinfo(lookup); - - if (!couldConnect) - { - if (env_found_empty("PG_REGRESS_SOCK_DIR")) - { - /* - * In test environment, in case of no internet access, just use the - * address of the non-loopback network interface. - */ - return fetchIPAddressFromInterfaceList(localIpAddress, size); - } - else - { - *mayRetry = true; - - if (strcmp(DEFAULT_INTERFACE_LOOKUP_SERVICE_NAME, serviceName) == 0) - { - log_level(logLevel, - "Failed to connect to \"%s\" on port %d " - "to discover this machine hostname, " - "please use --hostname", - serviceName, servicePort); - } - else - { - log_level(logLevel, - "Failed to connect to any of the IP addresses for " - "monitor hostname \"%s\" and port %d", - serviceName, servicePort); - } - return false; - } - } - - if (!ipaddr_getsockname(sock, localIpAddress, size)) - { - /* errors have already been logged */ - close(sock); - return false; - } - - close(sock); - - return true; -} - - -/* - * fetchLocalCIDR loops over the local interfaces on the host and finds the one - * for which the IP address is the same as the given localIpAddress parameter. - * Then using the netmask information from the network interface, - * fetchLocalCIDR computes the local CIDR to use in HBA in order to allow - * authentication of all servers in the local network. - */ -bool -fetchLocalCIDR(const char *localIpAddress, char *localCIDR, int size) -{ - char network[INET6_ADDRSTRLEN]; - struct ifaddrs *ifaddr, *ifa; - int prefix = 0; - bool found = false; - - if (getifaddrs(&ifaddr) == -1) - { - log_warn("Failed to get the list of local network inferfaces: %m"); - return false; - } - - for (ifa = ifaddr; ifa; ifa = ifa->ifa_next) - { - char netmask[INET6_ADDRSTRLEN] = { 0 }; - char address[INET6_ADDRSTRLEN] = { 0 }; - - /* - * Some interfaces might have an empty ifa_addr, such as when using the - * PPTP protocol. With a NULL ifa_addr we can't inquire about the IP - * address and its netmask to compute any CIDR notation, so we skip the - * entry. - */ - if (ifa->ifa_addr == NULL) - { - log_debug("Skipping interface \"%s\" with NULL ifa_addr", - ifa->ifa_name); - continue; - } - - switch (ifa->ifa_addr->sa_family) - { - case AF_INET: - { - struct sockaddr_in *netmask4 = - (struct sockaddr_in *) ifa->ifa_netmask; - struct sockaddr_in *address4 = - (struct sockaddr_in *) ifa->ifa_addr; - - struct in_addr s_network; - - if (inet_ntop(AF_INET, (void *) &netmask4->sin_addr, - netmask, INET_ADDRSTRLEN) == NULL) - { - /* just skip that entry then */ - log_trace("Failed to determine local network CIDR: %m"); - continue; - } - - if (inet_ntop(AF_INET, (void *) &address4->sin_addr, - address, INET_ADDRSTRLEN) == NULL) - { - /* just skip that entry then */ - log_trace("Failed to determine local network CIDR: %m"); - continue; - } - - - s_network.s_addr = - address4->sin_addr.s_addr & netmask4->sin_addr.s_addr; - - prefix = countSetBits(netmask4->sin_addr.s_addr); - - if (inet_ntop(AF_INET, (void *) &s_network, - network, INET_ADDRSTRLEN) == NULL) - { - /* just skip that entry then */ - log_trace("Failed to determine local network CIDR: %m"); - continue; - } - - break; - } - - case AF_INET6: - { - int i = 0; - struct sockaddr_in6 *netmask6 = - (struct sockaddr_in6 *) ifa->ifa_netmask; - struct sockaddr_in6 *address6 = - (struct sockaddr_in6 *) ifa->ifa_addr; - - struct in6_addr s_network; - - if (inet_ntop(AF_INET6, (void *) &netmask6->sin6_addr, - netmask, INET6_ADDRSTRLEN) == NULL) - { - /* just skip that entry then */ - log_trace("Failed to determine local network CIDR: %m"); - continue; - } - - if (inet_ntop(AF_INET6, (void *) &address6->sin6_addr, - address, INET6_ADDRSTRLEN) == NULL) - { - /* just skip that entry then */ - log_trace("Failed to determine local network CIDR: %m"); - continue; - } - - for (i = 0; i < sizeof(struct in6_addr); i++) - { - s_network.s6_addr[i] = - address6->sin6_addr.s6_addr[i] & - netmask6->sin6_addr.s6_addr[i]; - } - - prefix = countSetBitsv6(netmask6->sin6_addr.s6_addr); - - if (inet_ntop(AF_INET6, &s_network, - network, INET6_ADDRSTRLEN) == NULL) - { - /* just skip that entry then */ - log_trace("Failed to determine local network CIDR: %m"); - continue; - } - - break; - } - - default: - continue; - } - - if (strcmp(address, localIpAddress) == 0) - { - found = true; - break; - } - } - freeifaddrs(ifaddr); - - if (!found) - { - return false; - } - - sformat(localCIDR, size, "%s/%d", network, prefix); - - return true; -} - - -/* - * countSetBits return how many bits are set (to 1) in an integer. When given a - * netmask, that's the CIDR prefix. - */ -static unsigned int -countSetBits(unsigned int n) -{ - unsigned int count = 0; - - while (n) - { - count += n & 1; - n >>= 1; - } - - return count; -} - - -/* - * countSetBitsv6 returns how many bits are set (to 1) in an IPv6 address, an - * array of 16 unsigned char values. When given a netmask, that's the - * prefixlen. - */ -static unsigned int -countSetBitsv6(unsigned char *addr) -{ - int i = 0; - unsigned int count = 0; - - for (i = 0; i < 16; i++) - { - unsigned char n = addr[i]; - - while (n) - { - count += n & 1; - n >>= 1; - } - } - - return count; -} - - -/* - * Fetches the IP address of the first non-loopback interface with an ip4 - * address. - */ -static bool -fetchIPAddressFromInterfaceList(char *localIpAddress, int size) -{ - bool found = false; - struct ifaddrs *ifaddrList = NULL, *ifaddr = NULL; - - if (getifaddrs(&ifaddr) == -1) - { - log_error("Failed to get the list of local network inferfaces: %m"); - return false; - } - - for (ifaddr = ifaddrList; ifaddr != NULL; ifaddr = ifaddr->ifa_next) - { - if (ifaddr->ifa_flags & IFF_LOOPBACK) - { - log_trace("Skipping loopback interface \"%s\"", ifaddr->ifa_name); - continue; - } - - /* - * Some interfaces might have an empty ifa_addr, such as when using the - * PPTP protocol. With a NULL ifa_addr we can't inquire about the IP - * address and its netmask to compute any CIDR notation, so we skip the - * entry. - */ - if (ifaddr->ifa_addr == NULL) - { - log_debug("Skipping interface \"%s\" with NULL ifa_addr", - ifaddr->ifa_name); - continue; - } - - /* - * We only support IPv4 here, also this function is only called in test - * environment where we run in a docker container with a network - * namespace in which we use only IPv4, so that's ok. - */ - if (ifaddr->ifa_addr->sa_family == AF_INET) - { - struct sockaddr_in *ip = - (struct sockaddr_in *) ifaddr->ifa_addr; - - if (inet_ntop(AF_INET, (void *) &(ip->sin_addr), - localIpAddress, size) == NULL) - { - /* skip that address, silently */ - log_trace("Failed to determine local network CIDR: %m"); - continue; - } - - found = true; - break; - } - } - - freeifaddrs(ifaddrList); - - return found; -} - - -/* - * From /Users/dim/dev/PostgreSQL/postgresql/src/backend/libpq/hba.c - */ -static bool -ipv4eq(struct sockaddr_in *a, struct sockaddr_in *b) -{ - return (a->sin_addr.s_addr == b->sin_addr.s_addr); -} - - -/* - * From /Users/dim/dev/PostgreSQL/postgresql/src/backend/libpq/hba.c - */ -static bool -ipv6eq(struct sockaddr_in6 *a, struct sockaddr_in6 *b) -{ - int i; - - for (i = 0; i < 16; i++) - { - if (a->sin6_addr.s6_addr[i] != b->sin6_addr.s6_addr[i]) - { - return false; - } - } - - return true; -} - - -/* - * findHostnameLocalAddress does a reverse DNS lookup given a hostname - * (--hostname), and if the DNS lookup fails or doesn't return any local IP - * address, then returns false. - */ -bool -findHostnameLocalAddress(const char *hostname, char *localIpAddress, int size) -{ - struct addrinfo *dns_lookup_addr; - struct addrinfo *dns_addr; - struct ifaddrs *ifaddrList, *ifaddr; - - if (!GetAddrInfo(hostname, NULL, 0, &dns_lookup_addr)) - { - /* errors have already been logged */ - return false; - } - - /* - * Loop over DNS results for the given hostname. Filter out loopback - * devices, and for each IP address given by the look-up, check if we - * have a corresponding local interface bound to the IP address. - */ - if (getifaddrs(&ifaddrList) == -1) - { - log_warn("Failed to get the list of local network inferfaces: %m"); - return false; - } - - /* - * Compare both addresses list (dns lookup and list of interface - * addresses) in a nested loop fashion: lists are not sorted, and we - * expect something like a dozen entry per list anyway. - */ - for (dns_addr = dns_lookup_addr; - dns_addr != NULL; - dns_addr = dns_addr->ai_next) - { - for (ifaddr = ifaddrList; ifaddr != NULL; ifaddr = ifaddr->ifa_next) - { - /* - * Some interfaces might have an empty ifa_addr, such as when using - * the PPTP protocol. With a NULL ifa_addr we can't inquire about - * the IP address and its netmask to compute any CIDR notation, so - * we skip the entry. - */ - if (ifaddr->ifa_addr == NULL) - { - log_debug("Skipping interface \"%s\" with NULL ifa_addr", - ifaddr->ifa_name); - continue; - } - - if (ifaddr->ifa_addr->sa_family == AF_INET && - dns_addr->ai_family == AF_INET) - { - struct sockaddr_in *ip = - (struct sockaddr_in *) ifaddr->ifa_addr; - - if (ipv4eq(ip, (struct sockaddr_in *) dns_addr->ai_addr)) - { - /* - * Found an IP address in the DNS answer that - * matches one of the interfaces IP addresses on - * the machine. - */ - freeaddrinfo(dns_lookup_addr); - - if (inet_ntop(AF_INET, - (void *) &(ip->sin_addr), - localIpAddress, - size) == NULL) - { - log_warn("Failed to determine local ip address: %m"); - freeifaddrs(ifaddrList); - return false; - } - - freeifaddrs(ifaddrList); - return true; - } - } - else if (ifaddr->ifa_addr->sa_family == AF_INET6 && - dns_addr->ai_family == AF_INET6) - { - struct sockaddr_in6 *ip = - (struct sockaddr_in6 *) ifaddr->ifa_addr; - - if (ipv6eq(ip, (struct sockaddr_in6 *) dns_addr->ai_addr)) - { - /* - * Found an IP address in the DNS answer that - * matches one of the interfaces IP addresses on - * the machine. - */ - freeaddrinfo(dns_lookup_addr); - - if (inet_ntop(AF_INET6, - (void *) &(ip->sin6_addr), - localIpAddress, - size) == NULL) - { - /* check size >= INET6_ADDRSTRLEN */ - log_warn("Failed to determine local ip address: %m"); - freeifaddrs(ifaddrList); - return false; - } - - freeifaddrs(ifaddrList); - return true; - } - } - } - } - - freeifaddrs(ifaddrList); - freeaddrinfo(dns_lookup_addr); - return false; -} - - -/* - * ip_address_type parses the hostname and determines whether it is an IPv4 - * address, IPv6 address, or DNS name. - * - * To edit pg HBA file, when given an IP address (rather than a hostname), we - * need to compute the CIDR mask. In the case of ipv4, that's /32, in the case - * of ipv6, that's /128. The `ip_address_type' function discovers which type of - * IP address we are dealing with. - */ -IPType -ip_address_type(const char *hostname) -{ - struct in_addr ipv4; - struct in6_addr ipv6; - - if (hostname == NULL) - { - return IPTYPE_NONE; - } - else if (inet_pton(AF_INET, hostname, &ipv4) == 1) - { - log_trace("hostname \"%s\" is ipv4", hostname); - return IPTYPE_V4; - } - else if (inet_pton(AF_INET6, hostname, &ipv6) == 1) - { - log_trace("hostname \"%s\" is ipv6", hostname); - return IPTYPE_V6; - } - return IPTYPE_NONE; -} - - -/* - * findHostnameFromLocalIpAddress does a reverse DNS lookup from a given IP - * address, and returns the first hostname of the DNS response. - */ -bool -findHostnameFromLocalIpAddress(char *localIpAddress, char *hostname, int size) -{ - char hbuf[NI_MAXHOST]; - struct addrinfo *lookup, *ai; - - /* parse ipv4 or ipv6 address using getaddrinfo() */ - if (!GetAddrInfo(localIpAddress, NULL, 0, &lookup)) - { - /* errors have already been logged */ - return false; - } - - /* now reverse lookup (NI_NAMEREQD) the address with getnameinfo() */ - for (ai = lookup; ai; ai = ai->ai_next) - { - int ret = getnameinfo(ai->ai_addr, ai->ai_addrlen, - hbuf, sizeof(hbuf), NULL, 0, NI_NAMEREQD); - - if (ret != 0) - { - log_warn("Failed to resolve hostname from address \"%s\": %s", - localIpAddress, gai_strerror(ret)); - return false; - } - - sformat(hostname, size, "%s", hbuf); - - /* stop at the first hostname found */ - break; - } - freeaddrinfo(lookup); - - return true; -} - - -/* - * resolveHostnameForwardAndReverse returns true when we could do a forward DNS - * lookup for the hostname and one of the IP addresses from the lookup resolves - * back to the hostname when doing a reverse-DNS lookup from it. - * - * When Postgres runs the DNS checks in the HBA implementation, the client IP - * address is looked-up in a reverse DNS query, and that name is compared to - * the hostname in the HBA file. Then, a forward DNS query is performed on the - * hostname, and one of the IP addresses returned must match with the client IP - * address. - * - * client ip -- reverse dns lookup --> hostname - * hostname -- forward dns lookup --> { ... client ip ... } - * - * At this point we don't have a client IP address. That said, the Postgres - * check will always fail if we fail to get our hostname back from at least one - * of the IP addresses that our hostname forward-DNS query returns. - */ -bool -resolveHostnameForwardAndReverse(const char *hostname, char *ipaddr, int size, - bool *foundHostnameFromAddress) -{ - struct addrinfo *lookup, *ai; - - *foundHostnameFromAddress = false; - - if (!GetAddrInfo(hostname, NULL, 0, &lookup)) - { - /* errors have already been logged */ - return false; - } - - /* when everything fails, we return a proper empty string buffer */ - bzero((void *) ipaddr, size); - - /* loop over the forward DNS results for hostname */ - for (ai = lookup; ai; ai = ai->ai_next) - { - char candidateIPAddr[BUFSIZE] = { 0 }; - char hbuf[NI_MAXHOST] = { 0 }; - - if (!ipaddr_sockaddr_to_string(ai, candidateIPAddr, BUFSIZE)) - { - /* errors have already been logged */ - continue; - } - - /* keep the first IP address of the list */ - if (IS_EMPTY_STRING_BUFFER(ipaddr)) - { - strlcpy(ipaddr, candidateIPAddr, size); - } - - log_debug("%s has address %s", hostname, candidateIPAddr); - - /* now reverse lookup (NI_NAMEREQD) the address with getnameinfo() */ - int ret = getnameinfo(ai->ai_addr, ai->ai_addrlen, - hbuf, sizeof(hbuf), NULL, 0, NI_NAMEREQD); - - if (ret != 0) - { - log_debug("Failed to resolve hostname from address \"%s\": %s", - ipaddr, gai_strerror(ret)); - continue; - } - - log_debug("reverse lookup for \"%s\" gives \"%s\" first", - candidateIPAddr, hbuf); - - /* compare reverse-DNS lookup result with our hostname */ - if (strcmp(hbuf, hostname) == 0) - { - *foundHostnameFromAddress = true; - break; - } - } - freeaddrinfo(lookup); - - return true; -} - - -/* - * ipaddr_sockaddr_to_string converts a binary socket address to its string - * representation using inet_ntop(3). - */ -static bool -ipaddr_sockaddr_to_string(struct addrinfo *ai, char *ipaddr, size_t size) -{ - if (ai->ai_family == AF_INET) - { - struct sockaddr_in *ip = (struct sockaddr_in *) ai->ai_addr; - - if (inet_ntop(AF_INET, (void *) &(ip->sin_addr), ipaddr, size) == NULL) - { - log_debug("Failed to determine local ip address: %m"); - return false; - } - } - else if (ai->ai_family == AF_INET6) - { - struct sockaddr_in6 *ip = (struct sockaddr_in6 *) ai->ai_addr; - - if (inet_ntop(AF_INET6, (void *) &(ip->sin6_addr), ipaddr, size) == NULL) - { - log_debug("Failed to determine local ip address: %m"); - return false; - } - } - else - { - /* Highly unexpected */ - log_debug("Non supported ai_family %d", ai->ai_family); - return false; - } - - return true; -} - - -/* - * ipaddr_getsockname gets the IP address "name" from a connected socket. - */ -static bool -ipaddr_getsockname(int sock, char *ipaddr, size_t size) -{ - struct sockaddr_storage address = { 0 }; - socklen_t sockaddrlen = sizeof(address); - - - int err = getsockname(sock, (struct sockaddr *) (&address), &sockaddrlen); - if (err < 0) - { - log_warn("Failed to get IP address from socket: %m"); - return false; - } - - if (address.ss_family == AF_INET) - { - struct sockaddr_in *ip = (struct sockaddr_in *) &address; - - if (inet_ntop(AF_INET, (void *) &(ip->sin_addr), ipaddr, size) == NULL) - { - log_debug("Failed to determine local ip address: %m"); - return false; - } - } - else if (address.ss_family == AF_INET6) - { - struct sockaddr_in6 *ip = (struct sockaddr_in6 *) &address; - - if (inet_ntop(AF_INET6, (void *) &(ip->sin6_addr), ipaddr, size) == NULL) - { - log_debug("Failed to determine local ip address: %m"); - return false; - } - } - else - { - log_debug("Non supported ss_family %d", address.ss_family); - return false; - } - - return true; -} - - -/* - * ipaddrGetLocalHostname uses gethostname(3) to get the current machine - * hostname. We only use the result from gethostname(3) when in turn we can - * resolve the result to an IP address that is present on the local machine. - * - * Failing to match the hostname to a local IP address, we then use the default - * lookup service name and port instead (we would then connect to a google - * provided DNS service to see what is the default network interface/source - * address to connect to a remote endpoint; to avoid any of that process just - * using pg_autoctl with the --hostname option). - */ -bool -ipaddrGetLocalHostname(char *hostname, size_t size) -{ - char localIpAddress[BUFSIZE] = { 0 }; - char hostnameCandidate[_POSIX_HOST_NAME_MAX] = { 0 }; - - if (gethostname(hostnameCandidate, sizeof(hostnameCandidate)) == -1) - { - log_warn("Failed to get local hostname: %m"); - return false; - } - - log_debug("ipaddrGetLocalHostname: \"%s\"", hostnameCandidate); - - /* do a lookup of the host name and see that we get a local address back */ - if (!findHostnameLocalAddress(hostnameCandidate, localIpAddress, BUFSIZE)) - { - log_warn("Failed to get a local IP address for hostname \"%s\"", - hostnameCandidate); - return false; - } - - strlcpy(hostname, hostnameCandidate, size); - - return true; -} - - -/* - * GetAddrInfo calls getaddrinfo and implement a retry policy in case we get a - * transient failure from the system. And for kubernetes compatibility, we also - * retry when the plain EAI_FAIL error code is returned, because DNS entries in - * this environments are dynamic. - */ -static bool -GetAddrInfo(const char *restrict node, - const char *restrict service, - const struct addrinfo *restrict hints, - struct addrinfo **restrict res) -{ - bool success = false; - ConnectionRetryPolicy retryPolicy = { 0 }; - - (void) pgsql_set_interactive_retry_policy(&retryPolicy); - - while (!pgsql_retry_policy_expired(&retryPolicy)) - { - int error = getaddrinfo(node, service, hints, res); - - /* - * Given docker/kubernetes environments, we treat permanent DNS - * failures (EAI_FAIL) as a retryable condition, same as EAI_AGAIN. - */ - if (error != 0 && error != EAI_AGAIN && error != EAI_FAIL) - { - log_warn("Failed to resolve DNS name \"%s\": %s", - node, gai_strerror(error)); - return false; - } - else if (error != 0) - { - log_debug("Failed to resolve DNS name \"%s\": %s", - node, gai_strerror(error)); - } - - success = (error == 0); - - if (success) - { - break; - } - - int sleepTimeMs = - pgsql_compute_connection_retry_sleep_time(&retryPolicy); - - /* we have milliseconds, pg_usleep() wants microseconds */ - (void) pg_usleep(sleepTimeMs * 1000); - } - - return success; -} diff --git a/src/bin/pg_autoctl/ipaddr.h b/src/bin/pg_autoctl/ipaddr.h deleted file mode 100644 index 76cf2dbc6..000000000 --- a/src/bin/pg_autoctl/ipaddr.h +++ /dev/null @@ -1,40 +0,0 @@ -/* - * src/bin/pg_autoctl/ipaddr.h - * Find local ip used as source ip in ip packets, using getsockname and a udp - * connection. - * - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the PostgreSQL License. - * - */ - -#ifndef __IPADDRH__ -#define __IPADDRH__ - -#include - - -typedef enum -{ - IPTYPE_V4, IPTYPE_V6, IPTYPE_NONE -} IPType; - - -IPType ip_address_type(const char *hostname); -bool fetchLocalIPAddress(char *localIpAddress, int size, - const char *serviceName, int servicePort, - int logLevel, bool *mayRetry); -bool fetchLocalCIDR(const char *localIpAddress, char *localCIDR, int size); -bool findHostnameLocalAddress(const char *hostname, - char *localIpAddress, int size); -bool findHostnameFromLocalIpAddress(char *localIpAddress, - char *hostname, int size); - -bool resolveHostnameForwardAndReverse(const char *hostname, - char *ipaddr, int size, - bool *foundHostnameFromAddress); - -bool ipaddrGetLocalHostname(char *hostname, size_t size); - - -#endif /* __IPADDRH__ */ diff --git a/src/bin/pg_autoctl/lock_utils.c b/src/bin/pg_autoctl/lock_utils.c deleted file mode 100644 index cd65e0ee7..000000000 --- a/src/bin/pg_autoctl/lock_utils.c +++ /dev/null @@ -1,382 +0,0 @@ -/* - * src/bin/pg_autoctl/lock_utils.c - * Implementations of utility functions for inter-process locking - * - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the PostgreSQL License. - * - */ - -#include -#include -#include -#include -#include -#include -#include - -#include "defaults.h" -#include "file_utils.h" -#include "env_utils.h" -#include "lock_utils.h" -#include "log.h" -#include "pidfile.h" -#include "string_utils.h" - - -/* - * See man semctl(2) - */ -#if defined(__linux__) -union semun -{ - int val; - struct semid_ds *buf; - unsigned short *array; -}; -#endif - -/* - * semaphore_init creates or opens a named semaphore for the current process. - * - * We use the environment variable PG_AUTOCTL_SERVICE to signal when a process - * is a child process of the main pg_autoctl supervisor so that we are able to - * initialize our locking strategy before parsing the command line. After all, - * we might have to log some output during the parsing itself. - */ -bool -semaphore_init(Semaphore *semaphore) -{ - if (env_exists(PG_AUTOCTL_LOG_SEMAPHORE)) - { - return semaphore_open(semaphore); - } - else - { - bool success = semaphore_create(semaphore); - - /* - * Only the main process should unlink the semaphore at exit time. - * - * When we create a semaphore, ensure we put our semId in the expected - * environment variable (PG_AUTOCTL_LOG_SEMAPHORE), and we assign the - * current process' pid as the semaphore owner. - * - * When we open a pre-existing semaphore using PG_AUTOCTL_LOG_SEMAPHORE - * as the semId, the semaphore owner is left to zero. - * - * The atexit(3) function that removes the semaphores only acts when - * the owner is our current pid. That way, in case of an early failure - * in execv(), the semaphore is not dropped from under the main - * program. - * - * A typical way execv() would fail is when calling run_program() on a - * pathname that does not exists. - * - * Per atexit(3) manual page: - * - * When a child process is created via fork(2), it inherits copies of - * its parent's registrations. Upon a successful call to one of the - * exec(3) functions, all registrations are removed. - * - * And that's why it's important that we don't remove the semaphore in - * the atexit() cleanup function when a call to run_command() fails - * early. - */ - if (success) - { - IntString semIdString = intToString(semaphore->semId); - - setenv(PG_AUTOCTL_LOG_SEMAPHORE, semIdString.strValue, 1); - } - - return success; - } -} - - -/* - * semaphore_finish closes or unlinks given semaphore. - */ -bool -semaphore_finish(Semaphore *semaphore) -{ - /* - * At initialization time we either create a new semaphore and register - * getpid() as the owner, or we open a previously existing semaphore from - * its semId as found in our environment variable PG_AUTOCTL_LOG_SEMAPHORE. - * - * At finish time (called from the atexit(3) registry), we remove the - * semaphore only when we are the owner of it. - */ - if (semaphore->owner == getpid()) - { - return semaphore_unlink(semaphore); - } - - return true; -} - - -/* - * semaphore_create creates a new semaphore with the value 1. - */ -bool -semaphore_create(Semaphore *semaphore) -{ - union semun semun; - - semaphore->owner = getpid(); - semaphore->semId = semget(IPC_PRIVATE, 1, 0600); - - if (semaphore->semId < 0) - { - /* the semaphore_log_lock_function has not been set yet */ - log_fatal("Failed to create semaphore: %m\n"); - return false; - } - - /* to see this log line, change the default log level in set_logger() */ - log_trace("Created semaphore %d", semaphore->semId); - - semun.val = 1; - if (semctl(semaphore->semId, 0, SETVAL, semun) < 0) - { - /* the semaphore_log_lock_function has not been set yet */ - log_fatal("Failed to set semaphore %d/%d to value %d : %m\n", - semaphore->semId, 0, semun.val); - return false; - } - - return true; -} - - -/* - * semaphore_open opens our IPC_PRIVATE semaphore. - * - * We don't have a key for it, because we asked the kernel to create a new - * semaphore set with the guarantee that it would not exist already. So we - * re-use the semaphore identifier directly. - * - * We don't even have to call semget(2) here at all, because we share our - * semaphore identifier in the environment directly. - */ -bool -semaphore_open(Semaphore *semaphore) -{ - char semIdString[BUFSIZE] = { 0 }; - - /* ensure the owner is set to zero when we re-open an existing semaphore */ - semaphore->owner = 0; - - if (!get_env_copy(PG_AUTOCTL_LOG_SEMAPHORE, semIdString, BUFSIZE)) - { - /* errors have already been logged */ - return false; - } - - if (!stringToInt(semIdString, &semaphore->semId)) - { - /* errors have already been logged */ - return false; - } - - /* to see this log line, change the default log level in set_logger() */ - log_trace("Using semaphore %d", semaphore->semId); - - /* we have the semaphore identifier, no need to call semget(2), done */ - return true; -} - - -/* - * semaphore_unlink removes an existing named semaphore. - */ -bool -semaphore_unlink(Semaphore *semaphore) -{ - union semun semun; - - semun.val = 0; /* unused, but keep compiler quiet */ - - log_trace("ipcrm -s %d\n", semaphore->semId); - - if (semctl(semaphore->semId, 0, IPC_RMID, semun) < 0) - { - fformat(stderr, "Failed to remove semaphore %d: %m", semaphore->semId); - return false; - } - - return true; -} - - -/* - * semaphore_cleanup is used when we find a stale PID file, to remove a - * possibly left behind semaphore. The user could also use ipcs and ipcrm to - * figure that out, if the stale pidfile does not exist anymore. - */ -bool -semaphore_cleanup(const char *pidfile) -{ - Semaphore semaphore; - - long fileSize = 0L; - char *fileContents = NULL; - char *fileLines[BUFSIZE] = { 0 }; - - if (!file_exists(pidfile)) - { - return false; - } - - if (!read_file(pidfile, &fileContents, &fileSize)) - { - return false; - } - - int lineCount = splitLines(fileContents, fileLines, BUFSIZE); - - if (lineCount < PIDFILE_LINE_SEM_ID) - { - log_debug("Failed to cleanup the semaphore from stale pid file \"%s\": " - "it contains %d lines, semaphore id is expected in line %d", - pidfile, - lineCount, - PIDFILE_LINE_SEM_ID); - free(fileContents); - return false; - } - - if (!stringToInt(fileLines[PIDFILE_LINE_SEM_ID], &(semaphore.semId))) - { - /* errors have already been logged */ - free(fileContents); - return false; - } - - free(fileContents); - - log_trace("Read semaphore id %d from stale pidfile", semaphore.semId); - - return semaphore_unlink(&semaphore); -} - - -/* - * semaphore_lock locks a semaphore (decrement count), blocking if count would - * be < 0 - */ -bool -semaphore_lock(Semaphore *semaphore) -{ - int errStatus; - struct sembuf sops; - - sops.sem_op = -1; /* decrement */ - sops.sem_flg = SEM_UNDO; - sops.sem_num = 0; - - /* - * Note: if errStatus is -1 and errno == EINTR then it means we returned - * from the operation prematurely because we were sent a signal. So we - * try and lock the semaphore again. - * - * We used to check interrupts here, but that required servicing - * interrupts directly from signal handlers. Which is hard to do safely - * and portably. - */ - do { - errStatus = semop(semaphore->semId, &sops, 1); - } while (errStatus < 0 && errno == EINTR); - - if (errStatus < 0) - { - fformat(stderr, - "%d Failed to acquire a lock with semaphore %d: %m\n", - getpid(), - semaphore->semId); - return false; - } - - return true; -} - - -/* - * semaphore_unlock unlocks a semaphore (increment count) - */ -bool -semaphore_unlock(Semaphore *semaphore) -{ - int errStatus; - struct sembuf sops; - - sops.sem_op = 1; /* increment */ - sops.sem_flg = SEM_UNDO; - sops.sem_num = 0; - - /* - * Note: if errStatus is -1 and errno == EINTR then it means we returned - * from the operation prematurely because we were sent a signal. So we - * try and unlock the semaphore again. Not clear this can really happen, - * but might as well cope. - */ - do { - errStatus = semop(semaphore->semId, &sops, 1); - } while (errStatus < 0 && errno == EINTR); - - if (errStatus < 0) - { - fformat(stderr, - "Failed to release a lock with semaphore %d: %m\n", - semaphore->semId); - return false; - } - - return true; -} - - -/* - * semaphore_log_lock_function integrates our semaphore facility with the - * logging tool in use in this project. - */ -void -semaphore_log_lock_function(void *udata, int mode) -{ - Semaphore *semaphore = (Semaphore *) udata; - - /* - * If locking/unlocking fails for some weird reason, we still want to log. - * It's not so bad that we want to completely quit the program. - * That's why we ignore the return values of semaphore_unlock and - * semaphore_lock. - */ - - switch (mode) - { - /* unlock */ - case 0: - { - (void) semaphore_unlock(semaphore); - break; - } - - /* lock */ - case 1: - { - (void) semaphore_lock(semaphore); - break; - } - - default: - { - fformat(stderr, - "BUG: semaphore_log_lock_function called with mode %d", - mode); - exit(EXIT_CODE_INTERNAL_ERROR); - } - } -} diff --git a/src/bin/pg_autoctl/lock_utils.h b/src/bin/pg_autoctl/lock_utils.h deleted file mode 100644 index 465c15df7..000000000 --- a/src/bin/pg_autoctl/lock_utils.h +++ /dev/null @@ -1,40 +0,0 @@ -/* - * src/bin/pg_autoctl/lock_utils.h - * Utility functions for inter-process locking - * - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the PostgreSQL License. - * - */ - -#ifndef LOCK_UTILS_H -#define LOCK_UTILS_H - -#include - -#include -#include -#include - -typedef struct Semaphore -{ - int semId; - pid_t owner; -} Semaphore; - - -bool semaphore_init(Semaphore *semaphore); -bool semaphore_finish(Semaphore *semaphore); - -bool semaphore_create(Semaphore *semaphore); -bool semaphore_open(Semaphore *semaphore); -bool semaphore_unlink(Semaphore *semaphore); - -bool semaphore_cleanup(const char *pidfile); - -bool semaphore_lock(Semaphore *semaphore); -bool semaphore_unlock(Semaphore *semaphore); - -void semaphore_log_lock_function(void *udata, int mode); - -#endif /* LOCK_UTILS_H */ diff --git a/src/bin/pg_autoctl/parsing.c b/src/bin/pg_autoctl/parsing.c deleted file mode 100644 index 80d75a332..000000000 --- a/src/bin/pg_autoctl/parsing.c +++ /dev/null @@ -1,1221 +0,0 @@ -/* - * src/bin/pg_autoctl/parsing.c - * API for parsing the output of some PostgreSQL server commands. - * - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the PostgreSQL License. - * - */ - -#include -#include -#include -#include -#include -#include - -#include "parson.h" - -#include "log.h" -#include "nodestate_utils.h" -#include "parsing.h" -#include "string_utils.h" - -static bool parse_controldata_field_dbstate(const char *controlDataString, - DBState *state); - -static bool parse_controldata_field_uint32(const char *controlDataString, - const char *fieldName, - uint32_t *dest); - -static bool parse_controldata_field_uint64(const char *controlDataString, - const char *fieldName, - uint64_t *dest); - -static bool parse_controldata_field_lsn(const char *controlDataString, - const char *fieldName, - char lsn[]); - -static bool parse_bool_with_len(const char *value, size_t len, bool *result); - -static int nodeAddressCmpByNodeId(const void *a, const void *b); - -#define RE_MATCH_COUNT 10 - - -/* - * Simple Regexp matching that returns the first matching element. - */ -char * -regexp_first_match(const char *string, const char *regex) -{ - regex_t compiledRegex; - - regmatch_t m[RE_MATCH_COUNT]; - - if (string == NULL) - { - return NULL; - } - - int status = regcomp(&compiledRegex, regex, REG_EXTENDED | REG_NEWLINE); - - if (status != 0) - { - /* - * regerror() returns how many bytes are actually needed to host the - * error message, and truncates the error message when it doesn't fit - * in given size. If the message has been truncated, then we add an - * ellispis to our log entry. - * - * We could also dynamically allocate memory for the error message, but - * the error might be "out of memory" already... - */ - char message[BUFSIZE]; - size_t bytes = regerror(status, &compiledRegex, message, BUFSIZE); - - log_error("Failed to compile regex \"%s\": %s%s", - regex, message, bytes < BUFSIZE ? "..." : ""); - - regfree(&compiledRegex); - - return NULL; - } - - /* - * regexec returns 0 if the regular expression matches; otherwise, it - * returns a nonzero value. - */ - int matchStatus = regexec(&compiledRegex, string, RE_MATCH_COUNT, m, 0); - regfree(&compiledRegex); - - /* We're interested into 1. re matches 2. captured at least one group */ - if (matchStatus != 0 || m[0].rm_so == -1 || m[1].rm_so == -1) - { - return NULL; - } - else - { - regoff_t start = m[1].rm_so; - regoff_t finish = m[1].rm_eo; - int length = finish - start + 1; - char *result = (char *) malloc(length * sizeof(char)); - - if (result == NULL) - { - log_error(ALLOCATION_FAILED_ERROR); - return NULL; - } - - strlcpy(result, string + start, length); - - return result; - } - return NULL; -} - - -/* - * Parse the version number output from pg_ctl --version: - * pg_ctl (PostgreSQL) 10.3 - */ -bool -parse_version_number(const char *version_string, - char *pg_version_string, - size_t size, - int *pg_version) -{ - char *match = regexp_first_match(version_string, "([0-9.]+)"); - - if (match == NULL) - { - log_error("Failed to parse Postgres version number \"%s\"", - version_string); - return false; - } - - /* first, copy the version number in our expected result string buffer */ - strlcpy(pg_version_string, match, size); - - if (!parse_pg_version_string(pg_version_string, pg_version)) - { - /* errors have already been logged */ - free(match); - return false; - } - - free(match); - return true; -} - - -/* - * parse_dotted_version_string parses a major.minor dotted version string such - * as "12.6" into a single number in the same format as the pg_control_version, - * such as 1206. - */ -bool -parse_dotted_version_string(const char *pg_version_string, int *pg_version) -{ - /* now, parse the numbers into an integer, ala pg_control_version */ - bool dotFound = false; - char major[INTSTRING_MAX_DIGITS] = { 0 }; - char minor[INTSTRING_MAX_DIGITS] = { 0 }; - - int majorIdx = 0; - int minorIdx = 0; - - if (pg_version_string == NULL) - { - log_debug("BUG: parse_pg_version_string got NULL"); - return false; - } - - for (int i = 0; pg_version_string[i] != '\0'; i++) - { - if (pg_version_string[i] == '.') - { - if (dotFound) - { - log_error("Failed to parse Postgres version number \"%s\"", - pg_version_string); - return false; - } - - dotFound = true; - continue; - } - - if (dotFound) - { - minor[minorIdx++] = pg_version_string[i]; - } - else - { - major[majorIdx++] = pg_version_string[i]; - } - } - - /* Postgres alpha/beta versions report version "14" instead of "14.0" */ - if (!dotFound) - { - strlcpy(minor, "0", INTSTRING_MAX_DIGITS); - } - - int maj = 0; - int min = 0; - - if (!stringToInt(major, &maj) || - !stringToInt(minor, &min)) - { - log_error("Failed to parse Postgres version number \"%s\"", - pg_version_string); - return false; - } - - /* transform "12.6" into 1206, that is 12 * 100 + 6 */ - *pg_version = (maj * 100) + min; - - return true; -} - - -/* - * parse_pg_version_string parses a Postgres version string such as "12.6" into - * a single number in the same format as the pg_control_version, such as 1206. - */ -bool -parse_pg_version_string(const char *pg_version_string, int *pg_version) -{ - return parse_dotted_version_string(pg_version_string, pg_version); -} - - -/* - * parse_pgaf_version_string parses a pg_auto_failover version string such as - * "1.4" into a single number in the same format as the pg_control_version, - * such as 104. - */ -bool -parse_pgaf_extension_version_string(const char *version_string, int *version) -{ - return parse_dotted_version_string(version_string, version); -} - - -/* - * Parse the first 3 lines of output from pg_controldata: - * - * pg_control version number: 1002 - * Catalog version number: 201707211 - * Database system identifier: 6534312872085436521 - * - */ -bool -parse_controldata(PostgresControlData *pgControlData, - const char *control_data_string) -{ - if (!parse_controldata_field_dbstate(control_data_string, - &(pgControlData->state)) || - - !parse_controldata_field_uint32(control_data_string, - "pg_control version number", - &(pgControlData->pg_control_version)) || - - !parse_controldata_field_uint32(control_data_string, - "Catalog version number", - &(pgControlData->catalog_version_no)) || - - !parse_controldata_field_uint64(control_data_string, - "Database system identifier", - &(pgControlData->system_identifier)) || - - !parse_controldata_field_lsn(control_data_string, - "Latest checkpoint location", - pgControlData->latestCheckpointLSN) || - - !parse_controldata_field_uint32(control_data_string, - "Latest checkpoint's TimeLineID", - &(pgControlData->timeline_id))) - { - log_error("Failed to parse pg_controldata output"); - return false; - } - return true; -} - - -#define streq(x, y) ((x != NULL) && (y != NULL) && (strcmp(x, y) == 0)) - -/* - * parse_controldata_field_dbstate matches pg_controldata output for Database - * cluster state and fills in the value string as an enum value. - */ -static bool -parse_controldata_field_dbstate(const char *controlDataString, DBState *state) -{ - char regex[BUFSIZE] = { 0 }; - - sformat(regex, BUFSIZE, "Database cluster state: *(.*)$"); - - char *match = regexp_first_match(controlDataString, regex); - - if (match == NULL) - { - return false; - } - - if (streq(match, "starting up")) - { - *state = DB_STARTUP; - } - else if (streq(match, "shut down")) - { - *state = DB_SHUTDOWNED; - } - else if (streq(match, "shut down in recovery")) - { - *state = DB_SHUTDOWNED_IN_RECOVERY; - } - else if (streq(match, "shutting down")) - { - *state = DB_SHUTDOWNING; - } - else if (streq(match, "in crash recovery")) - { - *state = DB_IN_CRASH_RECOVERY; - } - else if (streq(match, "in archive recovery")) - { - *state = DB_IN_ARCHIVE_RECOVERY; - } - else if (streq(match, "in production")) - { - *state = DB_IN_PRODUCTION; - } - else - { - log_error("Failed to parse database cluster state \"%s\"", match); - free(match); - return false; - } - - free(match); - return true; -} - - -/* - * parse_controldata_field_uint32 matches pg_controldata output for a field - * name and gets its value as an uint64_t. It returns false when something went - * wrong, and true when the value can be used. - */ -static bool -parse_controldata_field_uint32(const char *controlDataString, - const char *fieldName, - uint32_t *dest) -{ - char regex[BUFSIZE]; - - sformat(regex, BUFSIZE, "^%s: *([0-9]+)$", fieldName); - char *match = regexp_first_match(controlDataString, regex); - - if (match == NULL) - { - return false; - } - - if (!stringToUInt32(match, dest)) - { - log_error("Failed to parse number \"%s\": %m", match); - free(match); - return false; - } - - free(match); - return true; -} - - -/* - * parse_controldata_field_uint64 matches pg_controldata output for a field - * name and gets its value as an uint64_t. It returns false when something went - * wrong, and true when the value can be used. - */ -static bool -parse_controldata_field_uint64(const char *controlDataString, - const char *fieldName, - uint64_t *dest) -{ - char regex[BUFSIZE]; - - sformat(regex, BUFSIZE, "^%s: *([0-9]+)$", fieldName); - char *match = regexp_first_match(controlDataString, regex); - - if (match == NULL) - { - return false; - } - - if (!stringToUInt64(match, dest)) - { - log_error("Failed to parse number \"%s\": %m", match); - free(match); - return false; - } - - free(match); - return true; -} - - -/* - * parse_controldata_field_lsn matches pg_controldata output for a field name - * and gets its value as a string, in an area that must be pre-allocated with - * at least PG_LSN_MAXLENGTH bytes. - */ -static bool -parse_controldata_field_lsn(const char *controlDataString, - const char *fieldName, - char lsn[]) -{ - char regex[BUFSIZE]; - - sformat(regex, BUFSIZE, "^%s: *([0-9A-F]+/[0-9A-F]+)$", fieldName); - char *match = regexp_first_match(controlDataString, regex); - - if (match == NULL) - { - return false; - } - - strlcpy(lsn, match, PG_LSN_MAXLENGTH); - - free(match); - return true; -} - - -/* - * parse_notification_message parses pgautofailover state change notifications, - * which are sent in the JSON format. - */ -bool -parse_state_notification_message(CurrentNodeState *nodeState, - const char *message) -{ - JSON_Value *json = json_parse_string(message); - JSON_Object *jsobj = json_value_get_object(json); - - log_trace("parse_state_notification_message: %s", message); - - if (json_type(json) != JSONObject) - { - log_error("Failed to parse JSON notification message: \"%s\"", message); - json_value_free(json); - return false; - } - - char *str = (char *) json_object_get_string(jsobj, "type"); - - if (strcmp(str, "state") != 0) - { - log_error("Failed to parse JSOBJ notification state message: " - "jsobj object type is not \"state\" as expected"); - json_value_free(json); - return false; - } - - str = (char *) json_object_get_string(jsobj, "formation"); - - if (str == NULL) - { - log_error("Failed to parse formation in JSON " - "notification message \"%s\"", message); - json_value_free(json); - return false; - } - strlcpy(nodeState->formation, str, sizeof(nodeState->formation)); - - double number = json_object_get_number(jsobj, "groupId"); - nodeState->groupId = (int) number; - - number = json_object_get_number(jsobj, "nodeId"); - nodeState->node.nodeId = (int) number; - - str = (char *) json_object_get_string(jsobj, "name"); - - if (str == NULL) - { - log_error("Failed to parse node name in JSON " - "notification message \"%s\"", message); - json_value_free(json); - return false; - } - strlcpy(nodeState->node.name, str, sizeof(nodeState->node.name)); - - str = (char *) json_object_get_string(jsobj, "host"); - - if (str == NULL) - { - log_error("Failed to parse node host in JSON " - "notification message \"%s\"", message); - json_value_free(json); - return false; - } - strlcpy(nodeState->node.host, str, sizeof(nodeState->node.host)); - - number = json_object_get_number(jsobj, "port"); - nodeState->node.port = (int) number; - - str = (char *) json_object_get_string(jsobj, "reportedState"); - - if (str == NULL) - { - log_error("Failed to parse reportedState in JSON " - "notification message \"%s\"", message); - json_value_free(json); - return false; - } - nodeState->reportedState = NodeStateFromString(str); - - str = (char *) json_object_get_string(jsobj, "goalState"); - - if (str == NULL) - { - log_error("Failed to parse goalState in JSON " - "notification message \"%s\"", message); - json_value_free(json); - return false; - } - nodeState->goalState = NodeStateFromString(str); - - str = (char *) json_object_get_string(jsobj, "health"); - - if (streq(str, "unknown")) - { - nodeState->health = -1; - } - else if (streq(str, "bad")) - { - nodeState->health = 0; - } - else if (streq(str, "good")) - { - nodeState->health = 1; - } - else - { - log_error("Failed to parse health in JSON " - "notification message \"%s\"", message); - json_value_free(json); - return false; - } - - json_value_free(json); - return true; -} - - -/* - * Try to interpret value as boolean value. Valid values are: true, - * false, yes, no, on, off, 1, 0; as well as unique prefixes thereof. - * If the string parses okay, return true, else false. - * If okay and result is not NULL, return the value in *result. - * - * Copied from PostgreSQL sources - * file : src/backend/utils/adt/bool.c - */ -static bool -parse_bool_with_len(const char *value, size_t len, bool *result) -{ - switch (*value) - { - case 't': - case 'T': - { - if (pg_strncasecmp(value, "true", len) == 0) - { - if (result) - { - *result = true; - } - return true; - } - break; - } - - case 'f': - case 'F': - { - if (pg_strncasecmp(value, "false", len) == 0) - { - if (result) - { - *result = false; - } - return true; - } - break; - } - - case 'y': - case 'Y': - { - if (pg_strncasecmp(value, "yes", len) == 0) - { - if (result) - { - *result = true; - } - return true; - } - break; - } - - case 'n': - case 'N': - { - if (pg_strncasecmp(value, "no", len) == 0) - { - if (result) - { - *result = false; - } - return true; - } - break; - } - - case 'o': - case 'O': - { - /* 'o' is not unique enough */ - if (pg_strncasecmp(value, "on", (len > 2 ? len : 2)) == 0) - { - if (result) - { - *result = true; - } - return true; - } - else if (pg_strncasecmp(value, "off", (len > 2 ? len : 2)) == 0) - { - if (result) - { - *result = false; - } - return true; - } - break; - } - - case '1': - { - if (len == 1) - { - if (result) - { - *result = true; - } - return true; - } - break; - } - - case '0': - { - if (len == 1) - { - if (result) - { - *result = false; - } - return true; - } - break; - } - - default: - { - break; - } - } - - if (result) - { - *result = false; /* suppress compiler warning */ - } - return false; -} - - -/* - * parse_bool parses boolean text value (true/false/on/off/yes/no/1/0) and - * puts the boolean value back in the result field if it is not NULL. - * The function returns true on successful parse, returns false if any parse - * error is encountered. - */ -bool -parse_bool(const char *value, bool *result) -{ - return parse_bool_with_len(value, strlen(value), result); -} - - -/* - * parse_pguri_info_key_vals decomposes elements of a Postgres connection - * string (URI) into separate arrays of keywords and values as expected by - * PQconnectdbParams. - */ -bool -parse_pguri_info_key_vals(const char *pguri, - KeyVal *overrides, - URIParams *uriParameters, - bool checkForCompleteURI) -{ - char *errmsg; - PQconninfoOption *conninfo, *option; - - bool foundHost = false; - bool foundUser = false; - bool foundPort = false; - bool foundDBName = false; - - int paramIndex = 0; - - conninfo = PQconninfoParse(pguri, &errmsg); - if (conninfo == NULL) - { - log_error("Failed to parse pguri \"%s\": %s", pguri, errmsg); - PQfreemem(errmsg); - return false; - } - - for (option = conninfo; option->keyword != NULL; option++) - { - char *value = NULL; - int ovIndex = 0; - - /* - * If the keyword is in our overrides array, use the value from the - * override values. Yeah that's O(n*m) but here m is expected to be - * something very small, like 3 (typically: sslmode, sslrootcert, - * sslcrl). - */ - for (ovIndex = 0; ovIndex < overrides->count; ovIndex++) - { - if (strcmp(overrides->keywords[ovIndex], option->keyword) == 0) - { - value = overrides->values[ovIndex]; - } - } - - /* not found in the override, keep the original, or skip */ - if (value == NULL) - { - if (option->val == NULL || strcmp(option->val, "") == 0) - { - continue; - } - else - { - value = option->val; - } - } - - if (strcmp(option->keyword, "host") == 0 || - strcmp(option->keyword, "hostaddr") == 0) - { - foundHost = true; - strlcpy(uriParameters->hostname, option->val, MAXCONNINFO); - } - else if (strcmp(option->keyword, "port") == 0) - { - foundPort = true; - strlcpy(uriParameters->port, option->val, MAXCONNINFO); - } - else if (strcmp(option->keyword, "user") == 0) - { - foundUser = true; - strlcpy(uriParameters->username, option->val, MAXCONNINFO); - } - else if (strcmp(option->keyword, "dbname") == 0) - { - foundDBName = true; - strlcpy(uriParameters->dbname, option->val, MAXCONNINFO); - } - else if (!IS_EMPTY_STRING_BUFFER(value)) - { - /* make a copy in our key/val arrays */ - strlcpy(uriParameters->parameters.keywords[paramIndex], - option->keyword, - MAXCONNINFO); - - strlcpy(uriParameters->parameters.values[paramIndex], - value, - MAXCONNINFO); - - ++uriParameters->parameters.count; - ++paramIndex; - } - } - - PQconninfoFree(conninfo); - - /* - * Display an error message per missing field, and only then return false - * if we're missing any one of those. - */ - if (checkForCompleteURI) - { - if (!foundHost) - { - log_error("Failed to find hostname in the pguri \"%s\"", pguri); - } - - if (!foundPort) - { - log_error("Failed to find port in the pguri \"%s\"", pguri); - } - - if (!foundUser) - { - log_error("Failed to find username in the pguri \"%s\"", pguri); - } - - if (!foundDBName) - { - log_error("Failed to find dbname in the pguri \"%s\"", pguri); - } - - return foundHost && foundPort && foundUser && foundDBName; - } - else - { - return true; - } -} - - -/* - * buildPostgresURIfromPieces builds a Postgres connection string from keywords - * and values, in a user friendly way. The pguri parameter should point to a - * memory area that has been allocated by the caller and has at least - * MAXCONNINFO bytes. - */ -bool -buildPostgresURIfromPieces(URIParams *uriParams, char *pguri) -{ - int index = 0; - - sformat(pguri, MAXCONNINFO, - "postgres://%s@%s:%s/%s?", - uriParams->username, - uriParams->hostname, - uriParams->port, - uriParams->dbname); - - for (index = 0; index < uriParams->parameters.count; index++) - { - if (index == 0) - { - sformat(pguri, MAXCONNINFO, - "%s%s=%s", - pguri, - uriParams->parameters.keywords[index], - uriParams->parameters.values[index]); - } - else - { - sformat(pguri, MAXCONNINFO, - "%s&%s=%s", - pguri, - uriParams->parameters.keywords[index], - uriParams->parameters.values[index]); - } - } - - return true; -} - - -/* - * parse_pguri_ssl_settings parses SSL settings from a Postgres connection - * string. Given the following connection string - * - * "postgres://autoctl_node@localhost:5500/pg_auto_failover?sslmode=prefer" - * - * we then have an ssl->active = 1, ssl->sslMode = SSL_MODE_PREFER, etc. - */ -bool -parse_pguri_ssl_settings(const char *pguri, SSLOptions *ssl) -{ - URIParams params = { 0 }; - KeyVal overrides = { 0 }; - - bool checkForCompleteURI = true; - - /* initialize SSL Params values */ - if (!parse_pguri_info_key_vals(pguri, - &overrides, - ¶ms, - checkForCompleteURI)) - { - /* errors have already been logged */ - return false; - } - - for (int index = 0; index < params.parameters.count; index++) - { - char *key = params.parameters.keywords[index]; - char *val = params.parameters.values[index]; - - if (streq(key, "sslmode")) - { - ssl->sslMode = pgsetup_parse_sslmode(val); - strlcpy(ssl->sslModeStr, val, sizeof(ssl->sslModeStr)); - - if (ssl->sslMode > SSL_MODE_DISABLE) - { - ssl->active = true; - } - } - else if (streq(key, "sslrootcert")) - { - strlcpy(ssl->caFile, val, sizeof(ssl->caFile)); - } - else if (streq(key, "sslcrl")) - { - strlcpy(ssl->crlFile, val, sizeof(ssl->crlFile)); - } - else if (streq(key, "sslcert")) - { - strlcpy(ssl->serverCert, val, sizeof(ssl->serverCert)); - } - else if (streq(key, "sslkey")) - { - strlcpy(ssl->serverKey, val, sizeof(ssl->serverKey)); - } - } - - /* cook-in defaults when the parsed URL contains no SSL settings */ - if (ssl->sslMode == SSL_MODE_UNKNOWN) - { - ssl->active = true; - ssl->sslMode = SSL_MODE_PREFER; - strlcpy(ssl->sslModeStr, - pgsetup_sslmode_to_string(ssl->sslMode), - sizeof(ssl->sslModeStr)); - } - - return true; -} - - -/* - * nodeAddressCmpByNodeId sorts two given nodeAddress by comparing their - * nodeId. We use this function to be able to pg_qsort() an array of nodes, - * such as when parsing from a JSON file. - */ -static int -nodeAddressCmpByNodeId(const void *a, const void *b) -{ - NodeAddress *nodeA = (NodeAddress *) a; - NodeAddress *nodeB = (NodeAddress *) b; - - return nodeA->nodeId - nodeB->nodeId; -} - - -/* - * parseLSN is based on the Postgres code for pg_lsn_in_internal found at - * src/backend/utils/adt/pg_lsn.c in the Postgres source repository. In the - * pg_auto_failover context we don't need to typedef uint64 XLogRecPtr; so we - * just use uint64_t internally. - */ -#define MAXPG_LSNCOMPONENT 8 - -bool -parseLSN(const char *str, uint64_t *lsn) -{ - int len1, - len2; - uint32 id, - off; - - /* Sanity check input format. */ - len1 = strspn(str, "0123456789abcdefABCDEF"); - if (len1 < 1 || len1 > MAXPG_LSNCOMPONENT || str[len1] != '/') - { - return false; - } - - len2 = strspn(str + len1 + 1, "0123456789abcdefABCDEF"); - if (len2 < 1 || len2 > MAXPG_LSNCOMPONENT || str[len1 + 1 + len2] != '\0') - { - return false; - } - - /* Decode result. */ - id = (uint32) strtoul(str, NULL, 16); - off = (uint32) strtoul(str + len1 + 1, NULL, 16); - *lsn = ((uint64) id << 32) | off; - - return true; -} - - -/* - * parseNodesArrayFromFile parses a Nodes Array from a JSON file, that contains - * an array of JSON object with the following properties: node_id, node_lsn, - * node_host, node_name, node_port, and potentially node_is_primary. - */ -bool -parseNodesArray(const char *nodesJSON, - NodeAddressArray *nodesArray, - int64_t nodeId) -{ - JSON_Value *template = - json_parse_string("[{" - "\"node_id\": 0," - "\"node_lsn\": \"\"," - "\"node_name\": \"\"," - "\"node_host\": \"\"," - "\"node_port\": 0," - "\"node_is_primary\": false" - "}]"); - int nodesArrayIndex = 0; - int primaryCount = 0; - - JSON_Value *json = json_parse_string(nodesJSON); - - /* validate the JSON input as an array of object with required fields */ - if (json_validate(template, json) == JSONFailure) - { - log_error("Failed to parse nodes array which is expected " - "to contain a JSON Array of Objects with properties " - "[{node_id:number, node_name:string, " - "node_host:string, node_port:number, node_lsn:string, " - "node_is_primary:boolean}, ...]"); - json_value_free(template); - json_value_free(json); - return false; - } - - JSON_Array *jsArray = json_value_get_array(json); - int len = json_array_get_count(jsArray); - - if (NODE_ARRAY_MAX_COUNT < len) - { - log_error("Failed to parse nodes array which contains " - "%d nodes: pg_autoctl supports up to %d nodes", - len, - NODE_ARRAY_MAX_COUNT); - json_value_free(template); - json_value_free(json); - return false; - } - - nodesArray->count = len; - - for (int i = 0; i < len; i++) - { - NodeAddress *node = &(nodesArray->nodes[nodesArrayIndex]); - JSON_Object *jsObj = json_array_get_object(jsArray, i); - - int jsNodeId = (int) json_object_get_number(jsObj, "node_id"); - uint64_t lsn = 0; - - /* we install the keeper.otherNodes array, so skip ourselves */ - if (jsNodeId == nodeId) - { - --(nodesArray->count); - continue; - } - - node->nodeId = jsNodeId; - - strlcpy(node->name, - json_object_get_string(jsObj, "node_name"), - sizeof(node->name)); - - strlcpy(node->host, - json_object_get_string(jsObj, "node_host"), - sizeof(node->host)); - - node->port = (int) json_object_get_number(jsObj, "node_port"); - - strlcpy(node->lsn, - json_object_get_string(jsObj, "node_lsn"), - sizeof(node->lsn)); - - if (!parseLSN(node->lsn, &lsn)) - { - log_error("Failed to parse nodes array LSN value \"%s\"", node->lsn); - json_value_free(template); - json_value_free(json); - return false; - } - - node->isPrimary = json_object_get_boolean(jsObj, "node_is_primary"); - - if (node->isPrimary) - { - ++primaryCount; - - if (primaryCount > 1) - { - log_error("Failed to parse nodes array: more than one node " - "is listed with \"node_is_primary\" true."); - json_value_free(template); - json_value_free(json); - return false; - } - } - - ++nodesArrayIndex; - } - - json_value_free(template); - json_value_free(json); - - /* now ensure the array is sorted by nodeId */ - (void) pg_qsort(nodesArray->nodes, - nodesArray->count, - sizeof(NodeAddress), - nodeAddressCmpByNodeId); - - /* check that every node id is unique in our array */ - for (int i = 0; i < (nodesArray->count - 1); i++) - { - int currentNodeId = nodesArray->nodes[i].nodeId; - int nextNodeId = nodesArray->nodes[i + 1].nodeId; - - if (currentNodeId == nextNodeId) - { - log_error("Failed to parse nodes array: more than one node " - "is listed with the same nodeId %d", - currentNodeId); - return false; - } - } - - return true; -} - - -/* - * uri_contains_password takes a Postgres connection string and checks to see - * if it contains a parameter called password. Returns true if a password - * keyword is present in the connection string. - */ -static bool -uri_contains_password(const char *pguri) -{ - char *errmsg; - PQconninfoOption *conninfo, *option; - - conninfo = PQconninfoParse(pguri, &errmsg); - if (conninfo == NULL) - { - log_error("Failed to parse pguri: %s", errmsg); - - PQfreemem(errmsg); - return false; - } - - /* - * Look for a populated password connection parameter - */ - for (option = conninfo; option->keyword != NULL; option++) - { - if (strcmp(option->keyword, "password") == 0 && - option->val != NULL && - !IS_EMPTY_STRING_BUFFER(option->val)) - { - PQconninfoFree(conninfo); - return true; - } - } - - PQconninfoFree(conninfo); - return false; -} - - -/* - * parse_and_scrub_connection_string takes a Postgres connection string and - * populates scrubbedPguri with the password replaced with **** for logging. - * The scrubbedPguri parameter should point to a memory area that has been - * allocated by the caller and has at least MAXCONNINFO bytes. - */ -bool -parse_and_scrub_connection_string(const char *pguri, char *scrubbedPguri) -{ - URIParams uriParams = { 0 }; - KeyVal overrides = { 0 }; - - if (uri_contains_password(pguri)) - { - overrides = (KeyVal) { - .count = 1, - .keywords = { "password" }, - .values = { "****" } - }; - } - - bool checkForCompleteURI = false; - - if (!parse_pguri_info_key_vals(pguri, - &overrides, - &uriParams, - checkForCompleteURI)) - { - return false; - } - - buildPostgresURIfromPieces(&uriParams, scrubbedPguri); - - return true; -} diff --git a/src/bin/pg_autoctl/parsing.h b/src/bin/pg_autoctl/parsing.h deleted file mode 100644 index 6b004549d..000000000 --- a/src/bin/pg_autoctl/parsing.h +++ /dev/null @@ -1,100 +0,0 @@ -/* - * src/bin/pg_autoctl/parsing.c - * API for parsing the output of some PostgreSQL server commands. - * - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the PostgreSQL License. - * - */ - -#ifndef PARSING_H -#define PARSING_H - -#include - -#include "monitor.h" -#include "nodestate_utils.h" -#include "pgctl.h" - -char * regexp_first_match(const char *string, const char *re); - -bool parse_version_number(const char *version_string, - char *pg_version_string, - size_t size, - int *pg_version); - -bool parse_dotted_version_string(const char *pg_version_string, - int *pg_version); -bool parse_pg_version_string(const char *pg_version_string, - int *pg_version); -bool parse_pgaf_extension_version_string(const char *version_string, - int *version); - -bool parse_controldata(PostgresControlData *pgControlData, - const char *control_data_string); - -bool parse_state_notification_message(CurrentNodeState *nodeState, - const char *message); - -bool parse_bool(const char *value, bool *result); - -#define boolToString(value) (value) ? "true" : "false" - - -/* - * To parse Postgres URI we need to store keywords and values in separate - * arrays of strings, because that's the libpq way of doing things. - * - * keywords and values are arrays of string and the arrays must be large enough - * to fit all the connection parameters (of which we count 36 at the moment on - * the Postgres documentation). - * - * See https://www.postgresql.org/docs/current/libpq-connect.html - * - * So here we use 64 entries each of MAXCONNINFO, to ensure we have enough room - * to store all the parts of a typicallay MAXCONNINFO bounded full URI. That - * amounts to 64kB of memory, so that's not even a luxury. - */ -typedef struct KeyVal -{ - int count; - char keywords[64][MAXCONNINFO]; - char values[64][MAXCONNINFO]; -} KeyVal; - - -/* - * In our own internal processing of Postgres URIs, we want to have some of the - * URL parts readily accessible by name rather than mixed in the KeyVal - * structure. - * - * That's mostly becase we want to produce an URI with the following form: - * - * postgres://user@host:port/dbname?opt=val - */ -typedef struct URIParams -{ - char username[MAXCONNINFO]; - char hostname[MAXCONNINFO]; - char port[MAXCONNINFO]; - char dbname[MAXCONNINFO]; - KeyVal parameters; -} URIParams; - -bool parse_pguri_info_key_vals(const char *pguri, - KeyVal *overrides, - URIParams *uriParameters, - bool checkForCompleteURI); - -bool buildPostgresURIfromPieces(URIParams *uriParams, char *pguri); - -bool parse_pguri_ssl_settings(const char *pguri, SSLOptions *ssl); - -bool parse_and_scrub_connection_string(const char *pguri, char *scrubbedPguri); - -bool parseLSN(const char *str, uint64_t *lsn); -bool parseNodesArray(const char *nodesJSON, - NodeAddressArray *nodesArray, - int64_t nodeId); - -#endif /* PARSING_H */ diff --git a/src/bin/pg_autoctl/pgctl.c b/src/bin/pg_autoctl/pgctl.c deleted file mode 100644 index 629726d1b..000000000 --- a/src/bin/pg_autoctl/pgctl.c +++ /dev/null @@ -1,2851 +0,0 @@ -/* - * src/bin/pg_autoctl/pgctl.c - * API for controling PostgreSQL, using its binary tooling (pg_ctl, - * pg_controldata, pg_basebackup and such). - * - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the PostgreSQL License. - * - */ - -#include -#include -#include -#include -#include -#include -#include - -#include "postgres_fe.h" -#include "pqexpbuffer.h" - -#include "defaults.h" -#include "env_utils.h" -#include "file_utils.h" -#include "log.h" -#include "parsing.h" -#include "pgctl.h" -#include "pgsql.h" -#include "pgsetup.h" -#include "pgtuning.h" -#include "signals.h" -#include "string_utils.h" - -#define RUN_PROGRAM_IMPLEMENTATION -#include "runprogram.h" - -#define AUTOCTL_CONF_INCLUDE_COMMENT \ - " # Auto-generated by pg_auto_failover, do not remove\n" - -#define AUTOCTL_CONF_INCLUDE_LINE "include '" AUTOCTL_DEFAULTS_CONF_FILENAME "'" -#define AUTOCTL_SB_CONF_INCLUDE_LINE "include '" AUTOCTL_STANDBY_CONF_FILENAME "'" - -static bool pg_include_config(const char *configFilePath, - const char *configIncludeLine, - const char *configIncludeComment); -static bool ensure_default_settings_file_exists(const char *configFilePath, - GUC *settings, - PostgresSetup *pgSetup, - const char *hostname, - bool includeTuning); -static bool prepare_guc_settings_from_pgsetup(const char *configFilePath, - PQExpBuffer config, - GUC *settings, - PostgresSetup *pgSetup, - const char *hostname, - bool includeTuning); -static void log_program_output(Program prog, int outLogLevel, int errorLogLevel); - - -static bool prepare_recovery_settings(const char *pgdata, - ReplicationSource *replicationSource, - char *primaryConnInfo, - char *primarySlotName, - char *targetLSN, - char *targetAction, - char *targetTimeline); - -static bool escape_recovery_conf_string(char *destination, - int destinationSize, - const char *recoveryConfString); -static bool prepare_primary_conninfo(char *primaryConnInfo, - int primaryConnInfoSize, - const char *primaryHost, int primaryPort, - const char *replicationUsername, - const char *dbname, - const char *replicationPassword, - const char *applicationName, - SSLOptions sslOptions, - bool escape); -static bool prepare_conninfo_sslmode(PQExpBuffer buffer, SSLOptions sslOptions); - -static bool pg_write_recovery_conf(const char *pgdata, - ReplicationSource *replicationSource); -static bool pg_write_standby_signal(const char *pgdata, - ReplicationSource *replicationSource); -static bool ensure_empty_tablespace_dirs(const char *pgdata); - -/* - * Get pg_ctl --version output in pgSetup->pg_version. - */ -bool -pg_ctl_version(PostgresSetup *pgSetup) -{ - Program prog = run_program(pgSetup->pg_ctl, "--version", NULL); - char pg_version_string[PG_VERSION_STRING_MAX] = { 0 }; - int pg_version = 0; - - if (prog.returnCode != 0) - { - errno = prog.error; - log_error("Failed to run \"pg_ctl --version\" using program \"%s\": %m", - pgSetup->pg_ctl); - free_program(&prog); - return false; - } - - if (!parse_version_number(prog.stdOut, - pg_version_string, - PG_VERSION_STRING_MAX, - &pg_version)) - { - /* errors have already been logged */ - free_program(&prog); - return false; - } - free_program(&prog); - - strlcpy(pgSetup->pg_version, pg_version_string, PG_VERSION_STRING_MAX); - - return true; -} - - -/* - * set_pg_ctl_from_PG_CONFIG sets given pgSetup->pg_ctl to the pg_ctl binary - * installed in the bindir of the target Postgres installation: - * - * $(${PG_CONFIG} --bindir)/pg_ctl - */ -bool -set_pg_ctl_from_config_bindir(PostgresSetup *pgSetup, const char *pg_config) -{ - char pg_ctl[MAXPGPATH] = { 0 }; - - if (!file_exists(pg_config)) - { - log_debug("set_pg_ctl_from_config_bindir: file not found: \"%s\"", - pg_config); - return false; - } - - Program prog = run_program(pg_config, "--bindir", NULL); - - char *lines[1]; - - if (prog.returnCode != 0) - { - errno = prog.error; - log_error("Failed to run \"pg_config --bindir\" using program \"%s\": %m", - pg_config); - free_program(&prog); - return false; - } - - if (splitLines(prog.stdOut, lines, 1) != 1) - { - log_error("Unable to parse output from pg_config --bindir"); - free_program(&prog); - return false; - } - - char *bindir = lines[0]; - join_path_components(pg_ctl, bindir, "pg_ctl"); - - /* we're now done with the Program and its output */ - free_program(&prog); - - if (!file_exists(pg_ctl)) - { - log_error("Failed to find pg_ctl at \"%s\" from PG_CONFIG at \"%s\"", - pgSetup->pg_ctl, - pg_config); - return false; - } - - strlcpy(pgSetup->pg_ctl, pg_ctl, sizeof(pgSetup->pg_ctl)); - - return true; -} - - -/* - * Read some of the information from pg_controldata output. - */ -bool -pg_controldata(PostgresSetup *pgSetup, bool missing_ok) -{ - char globalControlPath[MAXPGPATH] = { 0 }; - char pg_controldata_path[MAXPGPATH] = { 0 }; - - if (pgSetup->pgdata[0] == '\0' || pgSetup->pg_ctl[0] == '\0') - { - log_error("BUG: pg_controldata: missing pgSetup pgdata or pg_ctl"); - return false; - } - - /* globalControlFilePath = $PGDATA/global/pg_control */ - join_path_components(globalControlPath, pgSetup->pgdata, "global/pg_control"); - - /* - * Refrain from doing too many pg_controldata checks, only proceed when the - * PGDATA/global/pg_control file exists on-disk: that's the first check - * that pg_controldata does anyway. - */ - if (!file_exists(globalControlPath)) - { - return false; - } - - /* now find the pg_controldata binary */ - path_in_same_directory(pgSetup->pg_ctl, "pg_controldata", pg_controldata_path); - log_debug("%s %s", pg_controldata_path, pgSetup->pgdata); - - /* We parse the output of pg_controldata, make sure it's as expected */ - setenv("LANG", "C", 1); - Program prog = run_program(pg_controldata_path, pgSetup->pgdata, NULL); - - if (prog.returnCode == 0) - { - if (prog.stdOut == NULL) - { - /* happens sometimes, and I don't know why */ - log_warn("Got empty output from `%s %s`, trying again in 1s", - pg_controldata_path, pgSetup->pgdata); - sleep(1); - - return pg_controldata(pgSetup, missing_ok); - } - - if (!parse_controldata(&pgSetup->control, prog.stdOut)) - { - log_error("%s %s", pg_controldata_path, pgSetup->pgdata); - log_warn("Failed to parse pg_controldata output:\n%s", prog.stdOut); - free_program(&prog); - return false; - } - - free_program(&prog); - return true; - } - else - { - int errorLogLevel = missing_ok ? LOG_DEBUG : LOG_ERROR; - - (void) log_program_output(prog, LOG_INFO, errorLogLevel); - - log_level(errorLogLevel, - "Failed to run \"%s\" on \"%s\", see above for details", - pg_controldata_path, pgSetup->pgdata); - - free_program(&prog); - - return missing_ok; - } -} - - -/* - * set_pg_ctl_from_PG_CONFIG sets the path to pg_ctl following the exported - * environment variable PG_CONFIG, when it is found in the environment. - * - * Postgres developer environments often define PG_CONFIG in the environment to - * build extensions for a specific version of Postgres. Let's use the hint here - * too. - */ -bool -set_pg_ctl_from_PG_CONFIG(PostgresSetup *pgSetup) -{ - char PG_CONFIG[MAXPGPATH] = { 0 }; - - if (!env_exists("PG_CONFIG")) - { - /* then we don't use PG_CONFIG to find pg_ctl */ - return false; - } - - if (!get_env_copy("PG_CONFIG", PG_CONFIG, sizeof(PG_CONFIG))) - { - /* errors have already been logged */ - return false; - } - - if (!file_exists(PG_CONFIG)) - { - log_error("Failed to find a file for PG_CONFIG environment value \"%s\"", - PG_CONFIG); - return false; - } - - if (!set_pg_ctl_from_config_bindir(pgSetup, PG_CONFIG)) - { - /* errors have already been logged */ - return false; - } - - if (!pg_ctl_version(pgSetup)) - { - log_fatal("Failed to get version info from %s --version", - pgSetup->pg_ctl); - return false; - } - - log_debug("Found pg_ctl for PostgreSQL %s at %s following PG_CONFIG", - pgSetup->pg_version, pgSetup->pg_ctl); - - return true; -} - - -/* - * set_pg_ctl_from_pg_config sets the path to pg_ctl by using pg_config - * --bindir when there is a single pg_config found in the PATH. - * - * When using debian/ubuntu packaging then pg_config is installed as part as - * the postgresql-common package in /usr/bin, whereas pg_ctl is installed in a - * major version dependent location such as /usr/lib/postgresql/12/bin, and - * those locations are not included in the PATH. - * - * So when we can't find pg_ctl anywhere in the PATH, we look for pg_config - * instead, and then use pg_config --bindir to discover the pg_ctl we can use. - */ -bool -set_pg_ctl_from_pg_config(PostgresSetup *pgSetup) -{ - SearchPath all_pg_configs = { 0 }; - SearchPath pg_configs = { 0 }; - - if (!search_path("pg_config", &all_pg_configs)) - { - return false; - } - - if (!search_path_deduplicate_symlinks(&all_pg_configs, &pg_configs)) - { - log_error("Failed to resolve symlinks found in PATH entries, " - "see above for details"); - return false; - } - - switch (pg_configs.found) - { - case 0: - { - log_warn("Failed to find either pg_ctl or pg_config in PATH"); - return false; - } - - case 1: - { - if (!set_pg_ctl_from_config_bindir(pgSetup, pg_configs.matches[0])) - { - /* errors have already been logged */ - return false; - } - - if (!pg_ctl_version(pgSetup)) - { - log_fatal("Failed to get version info from %s --version", - pgSetup->pg_ctl); - return false; - } - - log_debug("Found pg_ctl for PostgreSQL %s at %s from pg_config " - "found in PATH at \"%s\"", - pgSetup->pg_version, - pgSetup->pg_ctl, - pg_configs.matches[0]); - - return true; - } - - default: - { - log_info("Found more than one pg_config entry in current PATH:"); - - for (int i = 0; i < pg_configs.found; i++) - { - PostgresSetup currentPgSetup = { 0 }; - - strlcpy(currentPgSetup.pg_ctl, - pg_configs.matches[i], - sizeof(currentPgSetup.pg_ctl)); - - if (!pg_ctl_version(¤tPgSetup)) - { - /* - * Because of this it's possible that there's now only a - * single working version of pg_ctl found in PATH. If - * that's the case we will still not use that by default, - * since the users intention is unclear. They might have - * wanted to use the version of pg_ctl that we could not - * parse the version string for. So we warn and continue, - * the user should make their intention clear by using the - * --pg_ctl option (or changing PATH). - */ - log_warn("Failed to get version info from %s --version", - currentPgSetup.pg_ctl); - continue; - } - - log_info("Found \"%s\" for pg version %s", - currentPgSetup.pg_ctl, - currentPgSetup.pg_version); - } - - log_info("HINT: export PG_CONFIG to a specific pg_config entry"); - - return false; - } - } - - return false; -} - - -/* - * Find "pg_ctl" programs in the PATH. If a single one exists, set its absolute - * location in pg_ctl, and the PostgreSQL version number in pg_version. - * - * Returns how many "pg_ctl" programs have been found in the PATH. - */ -bool -config_find_pg_ctl(PostgresSetup *pgSetup) -{ - SearchPath all_pg_ctls = { 0 }; - SearchPath pg_ctls = { 0 }; - - pgSetup->pg_ctl[0] = '\0'; - pgSetup->pg_version[0] = '\0'; - - /* - * Postgres developer environments often define PG_CONFIG in the - * environment to build extensions for a specific version of Postgres. - * Let's use the hint here too. - */ - if (set_pg_ctl_from_PG_CONFIG(pgSetup)) - { - return true; - } - - /* no PG_CONFIG. let's use the more classic approach with PATH instead */ - if (!search_path("pg_ctl", &all_pg_ctls)) - { - return false; - } - - if (!search_path_deduplicate_symlinks(&all_pg_ctls, &pg_ctls)) - { - log_error("Failed to resolve symlinks found in PATH entries, " - "see above for details"); - return false; - } - - if (pg_ctls.found == 1) - { - char *program = pg_ctls.matches[0]; - - strlcpy(pgSetup->pg_ctl, program, MAXPGPATH); - - if (!pg_ctl_version(pgSetup)) - { - log_fatal("Failed to get version info from \"%s\" --version", - pgSetup->pg_ctl); - return false; - } - - log_debug("Found pg_ctl for PostgreSQL %s at \"%s\"", - pgSetup->pg_version, - pgSetup->pg_ctl); - - return true; - } - else - { - /* - * Then, first look for pg_config --bindir with pg_config in PATH, - * we might have a single entry there, as is the case on a typical - * debian/ubuntu packaging, in /usr/bin/pg_config installed from - * the postgresql-common package. - */ - PostgresSetup pgSetupFromPgConfig = { 0 }; - - if (pg_ctls.found == 0) - { - log_debug("Failed to find pg_ctl in PATH, looking for pg_config"); - } - else - { - log_debug("Found %d entries for pg_ctl in PATH, " - "looking for pg_config", - pg_ctls.found); - } - - if (set_pg_ctl_from_pg_config(&pgSetupFromPgConfig)) - { - strlcpy(pgSetup->pg_ctl, - pgSetupFromPgConfig.pg_ctl, - sizeof(pgSetup->pg_ctl)); - - strlcpy(pgSetup->pg_version, - pgSetupFromPgConfig.pg_version, - sizeof(pgSetup->pg_version)); - return true; - } - - /* - * We failed to find a single pg_config in $PATH, error out and - * complain about the situation with enough details that the user - * can understand our struggle in picking a Postgres major version - * for them. - */ - log_info("Found more than one pg_ctl entry in current PATH, " - "and failed to find a single pg_config entry in current PATH"); - - for (int i = 0; i < pg_ctls.found; i++) - { - PostgresSetup currentPgSetup = { 0 }; - - strlcpy(currentPgSetup.pg_ctl, pg_ctls.matches[i], MAXPGPATH); - - if (!pg_ctl_version(¤tPgSetup)) - { - /* - * Because of this it's possible that there's now only a - * single working version of pg_ctl found in PATH. If - * that's the case we will still not use that by default, - * since the users intention is unclear. They might have - * wanted to use the version of pg_ctl that we could not - * parse the version string for. So we warn and continue, - * the user should make their intention clear by using the - * --pg_ctl option (or setting PG_CONFIG, or PATH). - */ - log_warn("Failed to get version info from \"%s\" --version", - currentPgSetup.pg_ctl); - continue; - } - - log_info("Found \"%s\" for pg version %s", - currentPgSetup.pg_ctl, - currentPgSetup.pg_version); - } - - log_error("Found several pg_ctl in PATH, please provide --pgctl"); - - return false; - } -} - - -/* - * find_pg_config_from_pg_ctl finds the path to pg_config from the known path - * to pg_ctl. If that exists, we first use the pg_config binary found in the - * same directory as the pg_ctl binary itself. - * - * Otherwise, we have a look at the PG_CONFIG environment variable. - * - * Finally, we search in the PATH list for all the matches, and for each of - * them we run pg_config --bindir, and if that's the directory where we have - * our known pg_ctl, that's our pg_config. - * - * Rationale: when using debian, the postgresql-common package installs a - * single entry for pg_config in /usr/bin/pg_config, and that's the system - * default. - * - * A version specific file path found in /usr/lib/postgresql/11/bin/pg_config - * when installing Postgres 11 is installed from the package - * postgresql-server-dev-11. - * - * There is no single default entry for pg_ctl, that said, so we are using the - * specific path /usr/lib/postgresql/11/bin/pg_config here. - * - * So depending on what packages have been deployed on this specific debian - * instance, we might or might not find a pg_config binary in the same - * directory as pg_ctl. - * - * Note that we could register the full path to whatever pg_config version we - * use at pg_autoctl create time, but in most cases that is going to be - * /usr/bin/pg_config, and it will point to a new pg_ctl (version 13 for - * instance) when you apt-get upgrade your debian testing distribution and it - * just migrated from Postgres 11 to Postgres 13 (bullseye cycle just did that - * in december 2020). - * - * Either package libpq-dev or postgresql-server-dev-11 (or another version) - * must be isntalled for this to work. - */ -bool -find_pg_config_from_pg_ctl(const char *pg_ctl, char *pg_config, size_t size) -{ - char pg_config_path[MAXPGPATH] = { 0 }; - - /* - * 1. try pg_ctl directory - */ - path_in_same_directory(pg_ctl, "pg_config", pg_config_path); - - if (file_exists(pg_config_path)) - { - log_debug("find_pg_config_from_pg_ctl: \"%s\" " - "in same directory as pg_ctl", - pg_config_path); - strlcpy(pg_config, pg_config_path, size); - return true; - } - - /* - * 2. try PG_CONFIG from the environment, and check pg_config --bindir - */ - if (env_exists("PG_CONFIG")) - { - PostgresSetup pgSetup = { 0 }; - char PG_CONFIG[MAXPGPATH] = { 0 }; - - /* check that the pg_config we found relates to the given pg_ctl */ - if (get_env_copy("PG_CONFIG", PG_CONFIG, sizeof(PG_CONFIG)) && - file_exists(PG_CONFIG) && - set_pg_ctl_from_config_bindir(&pgSetup, PG_CONFIG) && - strcmp(pgSetup.pg_ctl, pg_ctl) == 0) - { - log_debug("find_pg_config_from_pg_ctl: \"%s\" " - "from PG_CONFIG environment variable", - pg_config_path); - strlcpy(pg_config, pg_config_path, size); - return true; - } - } - - /* - * 3. search our PATH for pg_config entries and keep the first one that - * relates to our known pg_ctl. - */ - SearchPath all_pg_configs = { 0 }; - SearchPath pg_configs = { 0 }; - - if (!search_path("pg_config", &all_pg_configs)) - { - return false; - } - - if (!search_path_deduplicate_symlinks(&all_pg_configs, &pg_configs)) - { - log_error("Failed to resolve symlinks found in PATH entries, " - "see above for details"); - return false; - } - - for (int i = 0; i < pg_configs.found; i++) - { - PostgresSetup pgSetup = { 0 }; - - if (set_pg_ctl_from_config_bindir(&pgSetup, pg_configs.matches[i]) && - strcmp(pgSetup.pg_ctl, pg_ctl) == 0) - { - log_debug("find_pg_config_from_pg_ctl: \"%s\" " - "from PATH search", - pg_configs.matches[i]); - strlcpy(pg_config, pg_configs.matches[i], size); - return true; - } - } - - return false; -} - - -/* - * find_extension_control_file ensures that the extension is present in the - * given Postgres installation. This does the equivalent of: - * ls -l $(pg_config --sharedir)/extension/pg_stat_statements.control - */ -bool -find_extension_control_file(const char *pg_ctl, const char *extName) -{ - char pg_config_path[MAXPGPATH] = { 0 }; - char extension_path[MAXPGPATH] = { 0 }; - char *share_dir; - char extension_control_file_name[MAXPGPATH] = { 0 }; - char *lines[1]; - - log_debug("Checking if the %s extension is installed", extName); - - if (!find_pg_config_from_pg_ctl(pg_ctl, pg_config_path, MAXPGPATH)) - { - log_warn("Failed to find pg_config from pg_ctl at \"%s\"", pg_ctl); - return false; - } - - Program prog = run_program(pg_config_path, "--sharedir", NULL); - - if (prog.returnCode == 0) - { - if (!prog.stdOut) - { - log_error("Got empty output from pg_config --sharedir"); - free_program(&prog); - return false; - } - if (splitLines(prog.stdOut, lines, 1) != 1) - { - log_error("Unable to parse output from pg_config --sharedir"); - free_program(&prog); - return false; - } - share_dir = lines[0]; - - join_path_components(extension_path, share_dir, "extension"); - sformat(extension_control_file_name, MAXPGPATH, "%s.control", extName); - join_path_components(extension_path, extension_path, extension_control_file_name); - if (!file_exists(extension_path)) - { - log_error("Failed to find extension control file \"%s\"", - extension_path); - free_program(&prog); - return false; - } - } - else - { - (void) log_program_output(prog, LOG_INFO, LOG_ERROR); - log_error("Failed to run \"%s\", see above for details", - pg_config_path); - free_program(&prog); - return false; - } - free_program(&prog); - return true; -} - - -/* - * pg_add_auto_failover_default_settings ensures the pg_auto_failover default - * settings are included in postgresql.conf. For simplicity, this function - * reads the whole contents of postgresql.conf into memory. - */ -bool -pg_add_auto_failover_default_settings(PostgresSetup *pgSetup, - const char *hostname, - const char *configFilePath, - GUC *settings) -{ - bool includeTuning = true; - char pgAutoFailoverDefaultsConfigPath[MAXPGPATH]; - - /* - * Write the default settings to postgresql-auto-failover.conf. - * - * postgresql-auto-failover.conf needs to be placed alongside - * postgresql.conf for the include to work. Determine the path by finding - * the parent directory of postgresql.conf. - */ - path_in_same_directory(configFilePath, AUTOCTL_DEFAULTS_CONF_FILENAME, - pgAutoFailoverDefaultsConfigPath); - - if (!ensure_default_settings_file_exists(pgAutoFailoverDefaultsConfigPath, - settings, - pgSetup, - hostname, - includeTuning)) - { - return false; - } - - return pg_include_config(configFilePath, - AUTOCTL_CONF_INCLUDE_LINE, - AUTOCTL_CONF_INCLUDE_COMMENT); -} - - -/* - * pg_auto_failover_default_settings_file_exists returns true when our expected - * postgresql-auto-failover.conf file exists in PGDATA. - */ -bool -pg_auto_failover_default_settings_file_exists(PostgresSetup *pgSetup) -{ - char pgAutoFailoverDefaultsConfigPath[MAXPGPATH] = { 0 }; - char *contents = NULL; - long size = 0L; - - - join_path_components(pgAutoFailoverDefaultsConfigPath, - pgSetup->pgdata, - AUTOCTL_DEFAULTS_CONF_FILENAME); - - - /* make sure the file exists and is not empty (race conditions) */ - if (!read_file_if_exists(pgAutoFailoverDefaultsConfigPath, &contents, &size)) - { - return false; - } - - /* we don't actually need the contents here */ - free(contents); - bool fileExistsWithContent = size > 0; - - return fileExistsWithContent; -} - - -/* - * pg_include_config adds an include line to postgresql.conf to include the - * given configuration file, with a comment refering pg_auto_failover. - */ -static bool -pg_include_config(const char *configFilePath, - const char *configIncludeLine, - const char *configIncludeComment) -{ - char *currentConfContents = NULL; - long currentConfSize = 0L; - - /* read the current postgresql.conf contents */ - if (!read_file(configFilePath, ¤tConfContents, ¤tConfSize)) - { - return false; - } - - /* find the include 'postgresql-auto-failover.conf' line */ - char *includeLine = strstr(currentConfContents, configIncludeLine); - - if (includeLine != NULL && (includeLine == currentConfContents || - includeLine[-1] == '\n')) - { - log_debug("%s found in \"%s\"", configIncludeLine, configFilePath); - - /* defaults settings are already included */ - free(currentConfContents); - return true; - } - - log_debug("Adding %s to \"%s\"", configIncludeLine, configFilePath); - - /* build the new postgresql.conf contents */ - PQExpBuffer newConfContents = createPQExpBuffer(); - if (newConfContents == NULL) - { - log_error("Failed to allocate memory"); - free(currentConfContents); - return false; - } - - appendPQExpBufferStr(newConfContents, configIncludeLine); - appendPQExpBufferStr(newConfContents, configIncludeComment); - appendPQExpBufferStr(newConfContents, currentConfContents); - - /* done with the old postgresql.conf contents */ - free(currentConfContents); - - /* memory allocation could have failed while building string */ - if (PQExpBufferBroken(newConfContents)) - { - log_error("Failed to allocate memory"); - destroyPQExpBuffer(newConfContents); - return false; - } - - /* write the new postgresql.conf */ - if (!write_file(newConfContents->data, newConfContents->len, configFilePath)) - { - destroyPQExpBuffer(newConfContents); - return false; - } - - destroyPQExpBuffer(newConfContents); - - return true; -} - - -/* - * ensure_default_settings_file_exists writes the postgresql-auto-failover.conf - * file to the database directory. - */ -static bool -ensure_default_settings_file_exists(const char *configFilePath, - GUC *settings, - PostgresSetup *pgSetup, - const char *hostname, - bool includeTuning) -{ - PQExpBuffer defaultConfContents = createPQExpBuffer(); - - if (defaultConfContents == NULL) - { - log_error("Failed to allocate memory"); - return false; - } - - if (!prepare_guc_settings_from_pgsetup(configFilePath, - defaultConfContents, - settings, - pgSetup, - hostname, - includeTuning)) - { - /* errors have already been logged */ - destroyPQExpBuffer(defaultConfContents); - return false; - } - - if (file_exists(configFilePath)) - { - char *currentDefaultConfContents = NULL; - long currentDefaultConfSize = 0L; - - if (!read_file(configFilePath, ¤tDefaultConfContents, - ¤tDefaultConfSize)) - { - /* technically, we could still try writing, but this is pretty - * suspicious */ - destroyPQExpBuffer(defaultConfContents); - return false; - } - - if (strcmp(currentDefaultConfContents, defaultConfContents->data) == 0) - { - /* file is there and has the same contents, nothing to do */ - log_debug("Default settings file \"%s\" exists", configFilePath); - free(currentDefaultConfContents); - destroyPQExpBuffer(defaultConfContents); - return true; - } - - log_info("Contents of \"%s\" have changed, overwriting", configFilePath); - free(currentDefaultConfContents); - } - else - { - log_debug("Configuration file \"%s\" doesn't exists yet, creating", - configFilePath); - } - - if (!write_file(defaultConfContents->data, - defaultConfContents->len, - configFilePath)) - { - destroyPQExpBuffer(defaultConfContents); - return false; - } - - log_debug("Wrote file \"%s\" with content:\n%s", - configFilePath, defaultConfContents->data); - - destroyPQExpBuffer(defaultConfContents); - - return true; -} - - -/* - * prepare_guc_settings_from_pgsetup replaces some of the given GUC settings - * with dynamic values found in the pgSetup argument, and prepare them in the - * expected format for a postgresql.conf file in the given PQExpBuffer. - * - * While most of our settings are handle in a static way and thus known at - * compile time, some of them can be provided by our users, such as - * listen_addresses, port, and SSL related configuration parameters. - */ - -#define streq(x, y) ((x != NULL) && (y != NULL) && (strcmp(x, y) == 0)) - -static bool -prepare_guc_settings_from_pgsetup(const char *configFilePath, - PQExpBuffer config, - GUC *settings, - PostgresSetup *pgSetup, - const char *hostname, - bool includeTuning) -{ - char tuning[BUFSIZE] = { 0 }; - int settingIndex = 0; - - appendPQExpBufferStr(config, "# Settings by pg_auto_failover\n"); - - /* replace placeholder values with actual pgSetup values */ - for (settingIndex = 0; settings[settingIndex].name != NULL; settingIndex++) - { - GUC *setting = &settings[settingIndex]; - - /* - * Settings for "listen_addresses" and "port" are replaced with the - * respective values present in pgSetup allowing those to be dynamic. - * - * At the moment our "needs quote" heuristic is pretty simple. - * There's the one parameter within those that we hardcode from - * pg_auto_failover that needs quoting, and that's - * listen_addresses. - * - * The reason why POSTGRES_DEFAULT_LISTEN_ADDRESSES is not quoting - * the value directly in the constant is that we are using that - * value both in the configuration file and at the pg_ctl start - * --options "-h *" command line. - * - * At the command line, using --options "-h '*'" would give: - * could not create listen socket for "'*'" - */ - if (streq(setting->name, "listen_addresses")) - { - appendPQExpBuffer(config, "%s = '%s'\n", - setting->name, - pgSetup->listen_addresses); - } - else if (streq(setting->name, "password_encryption")) - { - /* - * Set password_encryption if the authMethod is password based. - */ - if (streq(pgSetup->authMethod, "md5") || - streq(pgSetup->authMethod, "scram-sha-256")) - { - appendPQExpBuffer(config, "%s = '%s'\n", - setting->name, - pgSetup->authMethod); - } - else if (streq(pgSetup->authMethod, "password")) - { - /* - * The "password" auth method supports only the "md5" and - * "scram-sha-256" password encryption settings. - * Default the encryption setting to "scram-sha-256" in this - * case, as it is the more secure alternative. - */ - appendPQExpBuffer(config, "%s = '%s'\n", - setting->name, - "scram-sha-256"); - } - } - else if (streq(setting->name, "port")) - { - appendPQExpBuffer(config, "%s = %d\n", - setting->name, - pgSetup->pgport); - } - else if (streq(setting->name, "ssl")) - { - appendPQExpBuffer(config, "%s = %s\n", - setting->name, - pgSetup->ssl.active == 0 ? "off" : "on"); - } - else if (streq(setting->name, "ssl_ca_file")) - { - if (!IS_EMPTY_STRING_BUFFER(pgSetup->ssl.caFile)) - { - appendPQExpBuffer(config, "%s = '%s'\n", - setting->name, pgSetup->ssl.caFile); - } - } - else if (streq(setting->name, "ssl_crl_file")) - { - if (!IS_EMPTY_STRING_BUFFER(pgSetup->ssl.crlFile)) - { - appendPQExpBuffer(config, "%s = '%s'\n", - setting->name, pgSetup->ssl.crlFile); - } - } - else if (streq(setting->name, "ssl_cert_file")) - { - if (!IS_EMPTY_STRING_BUFFER(pgSetup->ssl.serverCert)) - { - appendPQExpBuffer(config, "%s = '%s'\n", - setting->name, pgSetup->ssl.serverCert); - } - } - else if (streq(setting->name, "ssl_key_file")) - { - if (!IS_EMPTY_STRING_BUFFER(pgSetup->ssl.serverKey)) - { - appendPQExpBuffer(config, "%s = '%s'\n", - setting->name, pgSetup->ssl.serverKey); - } - } - else if (streq(setting->name, "recovery_target_lsn")) - { - if (streq(setting->value, "'immediate'")) - { - appendPQExpBuffer(config, "recovery_target = 'immediate'\n"); - } - else - { - appendPQExpBuffer(config, "%s = %s\n", - setting->name, setting->value); - } - } - else if (streq(setting->name, "citus.node_conninfo")) - { - appendPQExpBuffer(config, "%s = '", setting->name); - - /* add sslmode, sslrootcert, and sslcrl if needed */ - if (!prepare_conninfo_sslmode(config, pgSetup->ssl)) - { - /* errors have already been logged */ - return false; - } - - appendPQExpBufferStr(config, "'\n"); - } - else if (streq(setting->name, "citus.use_secondary_nodes")) - { - if (!IS_EMPTY_STRING_BUFFER(pgSetup->citusClusterName)) - { - appendPQExpBuffer(config, "%s = 'always'\n", setting->name); - } - } - else if (streq(setting->name, "citus.cluster_name")) - { - if (!IS_EMPTY_STRING_BUFFER(pgSetup->citusClusterName)) - { - appendPQExpBuffer(config, "%s = '%s'\n", - setting->name, pgSetup->citusClusterName); - } - } - else if (streq(setting->name, "citus.local_hostname")) - { - if (hostname != NULL && !IS_EMPTY_STRING_BUFFER(hostname)) - { - appendPQExpBuffer(config, "%s = '%s'\n", - setting->name, hostname); - } - } - else if (setting->value != NULL && - !IS_EMPTY_STRING_BUFFER(setting->value)) - { - appendPQExpBuffer(config, "%s = %s\n", - setting->name, - setting->value); - } - else if (setting->value == NULL || - IS_EMPTY_STRING_BUFFER(setting->value)) - { - /* - * Our GUC entry has a NULL (or empty) value. Skip the setting. - * - * In cases that's expected, such as when removing primary_conninfo - * from the recovery.conf settings so that we disconnect from the - * primary node being demoted. - * - * Still log about it, in case it might happen when it's not - * expected. - */ - log_debug("GUC setting \"%s\" has a NULL value", setting->name); - } - else - { - /* the GUC setting in the array has not been processed */ - log_error("BUG: GUC settings \"%s\" has not been processed", - setting->name); - return false; - } - } - - if (includeTuning) - { - if (!pgtuning_prepare_guc_settings(postgres_tuning, - tuning, - sizeof(tuning))) - { - log_warn("Failed to compute Postgres basic tuning for this system"); - } - - appendPQExpBuffer(config, "\n%s\n", tuning); - } - - /* memory allocation could have failed while building string */ - if (PQExpBufferBroken(config)) - { - log_error("Failed to allocate memory while preparing config file \"%s\"", - configFilePath); - destroyPQExpBuffer(config); - return false; - } - - return true; -} - - -bool -ensure_empty_tablespace_dirs(const char *pgdata) -{ - char *dirName = "pg_tblspc"; - struct dirent *dirEntry = NULL; - - char pgTblspcFullPath[MAXPGPATH] = { 0 }; - sformat(pgTblspcFullPath, MAXPGPATH, "%s/%s", pgdata, dirName); - - if (!directory_exists(pgTblspcFullPath)) - { - log_debug("Postgres dir pg_tblspc does not exist at \"%s\"", - pgTblspcFullPath); - return true; - } - - - /* open and scan through the Postgres tablespace directory */ - DIR *tblspcDir = opendir(pgTblspcFullPath); - - if (tblspcDir == NULL) - { - log_error("Failed to open Postgres tablespace directory \"%s\" at \"%s\"", - dirName, pgdata); - return false; - } - - while ((dirEntry = readdir(tblspcDir)) != NULL) - { - char tblspcDataDirPath[MAXPGPATH] = { 0 }; - - /* skip non-symlinks, as all tablespace dirs are symlinks */ - if (dirEntry->d_type == DT_UNKNOWN) - { - struct stat structStat; - char dirEntryFullPath[MAXPGPATH] = { 0 }; - sformat(dirEntryFullPath, MAXPGPATH, "%s/%s", pgTblspcFullPath, - dirEntry->d_name); - if (lstat(dirEntryFullPath, &structStat) != 0) - { - log_error("Failed to get file information for \"%s/%s\": %m", - pgTblspcFullPath, dirEntry->d_name); - return false; - } - if (!S_ISLNK(structStat.st_mode)) - { - log_debug("Non-symlink file found in tablespace directory: \"%s/%s\"", - pgTblspcFullPath, dirEntry->d_name); - continue; - } - } - else if (dirEntry->d_type != DT_LNK) - { - log_debug("Non-symlink file found in tablespace directory: \"%s/%s\"", - pgTblspcFullPath, dirEntry->d_name); - continue; - } - - join_path_components(tblspcDataDirPath, pgTblspcFullPath, dirEntry->d_name); - - log_debug("Removing contents of tablespace data directory \"%s\"", - tblspcDataDirPath); - - /* remove contents of tablespace data directory */ - if (!rmtree(tblspcDataDirPath, false)) - { - log_error( - "Failed to remove contents of existing tablespace data directory \"%s\": %m", - tblspcDataDirPath); - return false; - } - } - - return true; -} - - -/* - * Call pg_basebackup, using a temporary directory for the duration of the data - * transfer. - */ -bool -pg_basebackup(const char *pgdata, - const char *pg_ctl, - ReplicationSource *replicationSource) -{ - int returnCode; - char pg_basebackup[MAXPGPATH]; - - NodeAddress *primaryNode = &(replicationSource->primaryNode); - char primaryConnInfo[MAXCONNINFO] = { 0 }; - - char *args[16]; - int argsIndex = 0; - - char command[BUFSIZE]; - char pgpassword[BUFSIZE] = { 0 }; - - log_debug("mkdir -p \"%s\"", replicationSource->backupDir); - if (!ensure_empty_dir(replicationSource->backupDir, 0700)) - { - /* errors have already been logged. */ - return false; - } - - if (!ensure_empty_tablespace_dirs(pgdata)) - { - /* errors have already been logged. */ - return false; - } - - /* call pg_basebackup */ - path_in_same_directory(pg_ctl, "pg_basebackup", pg_basebackup); - - setenv("PGCONNECT_TIMEOUT", POSTGRES_CONNECT_TIMEOUT, 1); - - if (!IS_EMPTY_STRING_BUFFER(replicationSource->password)) - { - if (env_exists("PGPASSWORD")) - { - if (!get_env_copy("PGPASSWORD", pgpassword, sizeof(pgpassword))) - { - /* errors have already been logged. */ - return false; - } - } - setenv("PGPASSWORD", replicationSource->password, 1); - } - setenv("PGAPPNAME", replicationSource->applicationName, 1); - - if (!prepare_primary_conninfo(primaryConnInfo, - MAXCONNINFO, - primaryNode->host, - primaryNode->port, - replicationSource->userName, - NULL, /* no database */ - NULL, /* no password here */ - replicationSource->applicationName, - replicationSource->sslOptions, - false)) /* do not escape this one */ - { - /* errors have already been logged. */ - return false; - } - - args[argsIndex++] = (char *) pg_basebackup; - args[argsIndex++] = "-w"; - args[argsIndex++] = "-d"; - args[argsIndex++] = primaryConnInfo; - args[argsIndex++] = "--pgdata"; - args[argsIndex++] = replicationSource->backupDir; - args[argsIndex++] = "-U"; - args[argsIndex++] = replicationSource->userName; - args[argsIndex++] = "--verbose"; - args[argsIndex++] = "--progress"; - args[argsIndex++] = "--max-rate"; - args[argsIndex++] = replicationSource->maximumBackupRate; - args[argsIndex++] = "--wal-method=stream"; - - /* we don't use a replication slot e.g. when upstream is a standby */ - if (!IS_EMPTY_STRING_BUFFER(replicationSource->slotName)) - { - args[argsIndex++] = "--slot"; - args[argsIndex++] = replicationSource->slotName; - } - - args[argsIndex] = NULL; - - /* - * We do not want to call setsid() when running this program, as the - * pg_basebackup subprogram is not intended to be its own session leader, - * but remain a sub-process in the same group as pg_autoctl. - */ - Program program = { 0 }; - - (void) initialize_program(&program, args, false); - program.processBuffer = &processBufferCallback; - - /* log the exact command line we're using */ - int commandSize = snprintf_program_command_line(&program, command, BUFSIZE); - - if (commandSize >= BUFSIZE) - { - /* we only display the first BUFSIZE bytes of the real command */ - log_info("%s...", command); - } - else - { - log_info("%s", command); - } - - (void) execute_subprogram(&program); - - /* clean-up the environment again */ - if (!IS_EMPTY_STRING_BUFFER(replicationSource->password)) - { - if (IS_EMPTY_STRING_BUFFER(pgpassword)) - { - unsetenv("PGPASSWORD"); - } - else - { - setenv("PGPASSWORD", pgpassword, 1); - } - } - - returnCode = program.returnCode; - free_program(&program); - - if (returnCode != 0) - { - log_error("Failed to run pg_basebackup: exit code %d", returnCode); - return false; - } - - /* replace $pgdata with the backup directory */ - if (directory_exists(pgdata)) - { - if (!rmtree(pgdata, true)) - { - log_error("Failed to remove directory \"%s\": %m", pgdata); - return false; - } - } - - log_debug("mv \"%s\" \"%s\"", replicationSource->backupDir, pgdata); - - if (rename(replicationSource->backupDir, pgdata) != 0) - { - log_error( - "Failed to install pg_basebackup dir " " \"%s\" in \"%s\": %m", - replicationSource->backupDir, pgdata); - return false; - } - - return true; -} - - -/* - * pg_rewind runs the pg_rewind program to rewind the given database directory - * to a state where it can follow the given primary. We need the ability to - * connect to the node. - */ -bool -pg_rewind(const char *pgdata, - const char *pg_ctl, - ReplicationSource *replicationSource) -{ - int returnCode; - char pg_rewind[MAXPGPATH] = { 0 }; - - NodeAddress *primaryNode = &(replicationSource->primaryNode); - char primaryConnInfo[MAXCONNINFO] = { 0 }; - - char *args[7]; - int argsIndex = 0; - - char command[BUFSIZE]; - char pgpassword[BUFSIZE] = { 0 }; - - /* call pg_rewind*/ - path_in_same_directory(pg_ctl, "pg_rewind", pg_rewind); - - setenv("PGCONNECT_TIMEOUT", POSTGRES_CONNECT_TIMEOUT, 1); - - if (!IS_EMPTY_STRING_BUFFER(replicationSource->password)) - { - if (env_exists("PGPASSWORD")) - { - if (!get_env_copy("PGPASSWORD", pgpassword, sizeof(pgpassword))) - { - /* errors have already been logged. */ - return false; - } - } - setenv("PGPASSWORD", replicationSource->password, 1); - } - - if (!prepare_primary_conninfo(primaryConnInfo, - MAXCONNINFO, - primaryNode->host, - primaryNode->port, - replicationSource->userName, - "postgres", /* pg_rewind needs a database */ - NULL, /* no password here */ - replicationSource->applicationName, - replicationSource->sslOptions, - false)) /* do not escape this one */ - { - /* errors have already been logged. */ - return false; - } - - args[argsIndex++] = (char *) pg_rewind; - args[argsIndex++] = "--target-pgdata"; - args[argsIndex++] = (char *) pgdata; - args[argsIndex++] = "--source-server"; - args[argsIndex++] = primaryConnInfo; - args[argsIndex++] = "--progress"; - args[argsIndex] = NULL; - - /* - * We do not want to call setsid() when running this program, as the - * pg_rewind subprogram is not intended to be its own session leader, but - * remain a sub-process in the same group as pg_autoctl. - */ - Program program = { 0 }; - - (void) initialize_program(&program, args, false); - program.processBuffer = &processBufferCallback; - - /* log the exact command line we're using */ - int commandSize = snprintf_program_command_line(&program, command, BUFSIZE); - - if (commandSize >= BUFSIZE) - { - /* we only display the first BUFSIZE bytes of the real command */ - log_info("%s...", command); - } - else - { - log_info("%s", command); - } - - (void) execute_subprogram(&program); - - /* clean-up the environment again */ - if (!IS_EMPTY_STRING_BUFFER(replicationSource->password)) - { - if (IS_EMPTY_STRING_BUFFER(pgpassword)) - { - unsetenv("PGPASSWORD"); - } - else - { - setenv("PGPASSWORD", pgpassword, 1); - } - } - - returnCode = program.returnCode; - free_program(&program); - - if (returnCode != 0) - { - log_error("Failed to run pg_rewind: exit code %d", returnCode); - return false; - } - - return true; -} - - -/* log_program_output logs the output of the given program. */ -static void -log_program_output(Program prog, int outLogLevel, int errorLogLevel) -{ - if (prog.stdOut != NULL) - { - char *outLines[BUFSIZE]; - int lineCount = splitLines(prog.stdOut, outLines, BUFSIZE); - int lineNumber = 0; - - for (lineNumber = 0; lineNumber < lineCount; lineNumber++) - { - log_level(outLogLevel, "%s", outLines[lineNumber]); - } - } - - if (prog.stdErr != NULL) - { - char *errorLines[BUFSIZE]; - int lineCount = splitLines(prog.stdErr, errorLines, BUFSIZE); - int lineNumber = 0; - - for (lineNumber = 0; lineNumber < lineCount; lineNumber++) - { - log_level(errorLogLevel, "%s", errorLines[lineNumber]); - } - } -} - - -/* - * pg_ctl_initdb initializes a PostgreSQL directory from scratch by calling - * "pg_ctl initdb", and returns true when this was successful. Beware that it - * will inherit from the environment, such as LC_COLLATE and LC_ALL etc. - * - * No provision is made to control (sanitize?) that environment. - */ -bool -pg_ctl_initdb(const char *pg_ctl, const char *pgdata) -{ - /* initdb takes time, so log about the operation BEFORE doing it */ - log_info("Initialising a PostgreSQL cluster at \"%s\"", pgdata); - log_info("%s initdb -s -D %s --option '--auth=trust'", pg_ctl, pgdata); - - Program program = run_program(pg_ctl, - "--silent", - "--pgdata", pgdata, - - /* avoid warning message */ - "--option", "'--auth=trust'", "initdb", - NULL); - - bool success = program.returnCode == 0; - - if (program.returnCode != 0) - { - (void) log_program_output(program, LOG_INFO, LOG_ERROR); - log_fatal("Failed to initialize Postgres cluster at \"%s\", " - "see above for details", - pgdata); - } - else - { - /* we might still have important information to read there */ - (void) log_program_output(program, LOG_INFO, LOG_WARN); - } - free_program(&program); - - return success; -} - - -/* - * pg_ctl_postgres runs the "postgres" command-line in the current process, - * with the same options as we would use in pg_ctl_start. pg_ctl_postgres does - * not fork a Postgres process in the background, we keep the control over the - * postmaster process. Think exec() rather then fork(). - * - * This function will take over the current standard output and standard error - * file descriptor, closing them and then giving control to them to Postgres - * itself. This function is meant to be called in the child process of a fork() - * call done by the caller. - */ -bool -pg_ctl_postgres(const char *pg_ctl, const char *pgdata, int pgport, - char *listen_addresses, bool listen) -{ - char postgres[MAXPGPATH]; - char logfile[MAXPGPATH]; - - char *args[12]; - int argsIndex = 0; - - char env_pg_regress_sock_dir[MAXPGPATH]; - - char command[BUFSIZE]; - - /* call postgres directly */ - path_in_same_directory(pg_ctl, "postgres", postgres); - - /* prepare startup.log file in PGDATA */ - join_path_components(logfile, pgdata, "startup.log"); - - args[argsIndex++] = (char *) postgres; - args[argsIndex++] = "-D"; - args[argsIndex++] = (char *) pgdata; - args[argsIndex++] = "-p"; - args[argsIndex++] = (char *) intToString(pgport).strValue; - - if (listen) - { - if (IS_EMPTY_STRING_BUFFER(listen_addresses)) - { - log_error("BUG: pg_ctl_postgres is given an empty listen_addresses " - "with argument listen set to true"); - return false; - } - args[argsIndex++] = "-h"; - args[argsIndex++] = (char *) listen_addresses; - } - else - { - args[argsIndex++] = "-h"; - args[argsIndex++] = ""; - } - - if (env_exists("PG_REGRESS_SOCK_DIR")) - { - if (!get_env_copy("PG_REGRESS_SOCK_DIR", env_pg_regress_sock_dir, - MAXPGPATH)) - { - /* errors have already been logged */ - return false; - } - - args[argsIndex++] = "-k"; - args[argsIndex++] = (char *) env_pg_regress_sock_dir; - } - - args[argsIndex] = NULL; - - /* - * We do not want to call setsid() when running this program, as the - * postgres subprogram is not intended to be its own session leader, but - * remain a sub-process in the same group as pg_autoctl. - */ - Program program = { 0 }; - - (void) initialize_program(&program, args, false); - - /* we want to redirect the output to logfile */ - int logFileDescriptor = open(logfile, FOPEN_FLAGS_W, 0644); - - if (logFileDescriptor == -1) - { - log_error("Failed to open file \"%s\": %m", logfile); - } - - program.capture = false; /* redirect output, don't capture */ - program.stdOutFd = logFileDescriptor; - program.stdErrFd = logFileDescriptor; - - /* log the exact command line we're using */ - int commandSize = snprintf_program_command_line(&program, command, BUFSIZE); - - if (commandSize >= BUFSIZE) - { - /* we only display the first BUFSIZE bytes of the real command */ - log_info("%s...", command); - } - else - { - log_info("%s", command); - } - - (void) execute_program(&program); - - return program.returnCode == 0; -} - - -/* - * pg_log_startup logs the PGDATA/startup.log file contents so that our users - * have enough information about why Postgres failed to start when that - * happens. - */ -bool -pg_log_startup(const char *pgdata, int logLevel) -{ - char pgLogDirPath[MAXPGPATH] = { 0 }; - - char pgStartupPath[MAXPGPATH] = { 0 }; - char *fileContents; - long fileSize; - - /* logLevel to use when introducing which file path logs come from */ - int pathLogLevel = logLevel <= LOG_DEBUG ? LOG_DEBUG : LOG_WARN; - - struct stat pgStartupStat; - - struct dirent *logFileDirEntry = NULL; - - /* prepare startup.log file in PGDATA */ - join_path_components(pgStartupPath, pgdata, "startup.log"); - - if (read_file(pgStartupPath, &fileContents, &fileSize) && fileSize > 0) - { - char *lines[BUFSIZE]; - int lineCount = splitLines(fileContents, lines, BUFSIZE); - int lineNumber = 0; - - log_level(pathLogLevel, "Postgres logs from \"%s\":", pgStartupPath); - - for (lineNumber = 0; lineNumber < lineCount; lineNumber++) - { - log_level(logLevel, "%s", lines[lineNumber]); - } - - free(fileContents); - } - - /* - * Add in the most recent Postgres log file if it's been created after the - * startup.log file, it might contain very useful information, such as a - * FATAL line(s). - * - * Given that we setup Postgres to use the logging_collector, we expect - * there to be a single Postgres log file in the "log" directory that was - * created later than the "startup.log" file, and we expect the file to be - * rather short. - * - * Also we setup log_directory to be "log" so that's where we are looking - * into. - */ - - /* prepare PGDATA/log directory path */ - join_path_components(pgLogDirPath, pgdata, "log"); - - if (!directory_exists(pgLogDirPath)) - { - /* then there's no other log files to process here */ - return true; - } - - /* get the time of last modification of the startup.log file */ - if (lstat(pgStartupPath, &pgStartupStat) != 0) - { - log_error("Failed to get file information for \"%s\": %m", - pgStartupPath); - return false; - } - int64_t pgStartupMtime = ST_MTIME_S(pgStartupStat); - - /* open and scan through the Postgres log directory */ - DIR *logDir = opendir(pgLogDirPath); - - if (logDir == NULL) - { - log_error("Failed to open Postgres log directory \"%s\": %m", - pgLogDirPath); - return false; - } - - while ((logFileDirEntry = readdir(logDir)) != NULL) - { - char pgLogFilePath[MAXPGPATH] = { 0 }; - struct stat pgLogFileStat; - - - /* build the absolute file path for the logfile */ - join_path_components(pgLogFilePath, - pgLogDirPath, - logFileDirEntry->d_name); - - /* get the file information for the current logFile */ - if (lstat(pgLogFilePath, &pgLogFileStat) != 0) - { - log_error("Failed to get file information for \"%s\": %m", - pgLogFilePath); - return false; - } - - /* - * our logFiles are regular files, skip . and .. and others - * first, check for systems that do not handle d_type, and skip non-regular types - */ - if (logFileDirEntry->d_type == DT_UNKNOWN) - { - if (!S_ISREG(pgLogFileStat.st_mode)) - { - continue; - } - } - - /* - * next, ignore all other non-regular types - * (if this check were first, we would skip all with DT_UNKNOWN) - */ - else if (logFileDirEntry->d_type != DT_REG) - { - continue; - } - - int64_t pgLogFileMtime = ST_MTIME_S(pgLogFileStat); - - /* - * Compare modification times and only add to our logs the content - * from the Postgres log file that was created after the - * startup.log file. - */ - if (pgLogFileMtime >= pgStartupMtime) - { - log_level(pathLogLevel, - "Postgres logs from \"%s\":", pgLogFilePath); - - if (read_file(pgLogFilePath, &fileContents, &fileSize) && - fileSize > 0) - { - char *lines[BUFSIZE]; - int lineCount = splitLines(fileContents, lines, BUFSIZE); - int lineNumber = 0; - - for (lineNumber = 0; lineNumber < lineCount; lineNumber++) - { - if (strstr(lines[lineNumber], "FATAL") != NULL) - { - log_fatal("%s", lines[lineNumber]); - } - else if (strstr(lines[lineNumber], "ERROR") != NULL) - { - log_error("%s", lines[lineNumber]); - } - else - { - log_level(logLevel, "%s", lines[lineNumber]); - } - } - - free(fileContents); - } - } - } - - closedir(logDir); - - /* now add the contents of the recovery configuration */ - (void) pg_log_recovery_setup(pgdata, logLevel); - - return true; -} - - -/* - * pg_log_recovery_setup logs the current Postgres recovery settings from - * either the recovery.conf file or the standby setup. In case things go wrong - * in the Postgres version detection mechanism, or upgrades, or clean-up, this - * logs all the configuration files found rather than only those we expect we - * should find. - */ -bool -pg_log_recovery_setup(const char *pgdata, int logLevel) -{ - char *filenames[] = { - "recovery.conf", - "standby.signal", - AUTOCTL_STANDBY_CONF_FILENAME, - NULL - }; - - for (int i = 0; filenames[i] != NULL; i++) - { - char recoveryConfPath[MAXPGPATH] = { 0 }; - char *fileContents; - long fileSize; - - join_path_components(recoveryConfPath, pgdata, filenames[i]); - - if (file_exists(recoveryConfPath)) - { - if (!read_file(recoveryConfPath, &fileContents, &fileSize)) - { - /* errors have already been logged */ - continue; - } - - if (fileSize > 0) - { - log_debug("Configuration file \"%s\":\n%s", - recoveryConfPath, fileContents); - } - else - { - log_debug("Configuration file \"%s\" is empty", - recoveryConfPath); - } - - free(fileContents); - } - } - - return true; -} - - -/* - * pg_ctl_stop tries to stop a PostgreSQL server by running a "pg_ctl stop" - * command. If the server was stopped successfully, or if the server is not - * running at all, it returns true. - */ -bool -pg_ctl_stop(const char *pg_ctl, const char *pgdata) -{ - const bool log_output = true; - - log_info("%s --pgdata %s --wait stop --mode fast", pg_ctl, pgdata); - - Program program = run_program(pg_ctl, - "--pgdata", pgdata, - "--wait", - "--mode", "fast", - "stop", - NULL); - - /* - * Case 1. "pg_ctl stop" was successful, so we could stop the PostgreSQL - * server successfully. - */ - if (program.returnCode == 0) - { - free_program(&program); - return true; - } - - /* - * Case 2. The data directory doesn't exist. So we assume PostgreSQL is - * not running, so stopping the PostgreSQL server was successful. - */ - bool pgdata_exists = directory_exists(pgdata); - if (!pgdata_exists) - { - log_info("pgdata \"%s\" does not exist, consider this as PostgreSQL " - "not running", pgdata); - free_program(&program); - return true; - } - - /* - * Case 3. "pg_ctl stop" returns non-zero return code when PostgreSQL is not - * running at all. So we double-check with "pg_ctl status", and return - * success if the PostgreSQL server is not running. Otherwise, we return - * failure. - * - * See https://www.postgresql.org/docs/current/static/app-pg-ctl.html - */ - - int status = pg_ctl_status(pg_ctl, pgdata, log_output); - if (status == PG_CTL_STATUS_NOT_RUNNING) - { - log_info("pg_ctl stop failed, but PostgreSQL is not running anyway"); - free_program(&program); - return true; - } - - log_info("Stopping PostgreSQL server failed. pg_ctl status returned: %d", - status); - - if (log_output) - { - (void) log_program_output(program, LOG_INFO, LOG_ERROR); - } - - free_program(&program); - return false; -} - - -/* - * pg_ctl_status gets the status of the PostgreSQL server by running - * "pg_ctl status". Output of this command is logged if log_output is true. - * Return code of this command is returned. - */ -int -pg_ctl_status(const char *pg_ctl, const char *pgdata, bool log_output) -{ - Program program = run_program(pg_ctl, "-D", pgdata, "status", NULL); - int returnCode = program.returnCode; - - log_level(log_output ? LOG_INFO : LOG_DEBUG, - "%s status -D %s [%d]", pg_ctl, pgdata, returnCode); - - if (log_output) - { - (void) log_program_output(program, LOG_INFO, LOG_ERROR); - } - - free_program(&program); - return returnCode; -} - - -/* - * pg_ctl_reload reloads PostgreSQL configuration by running "pg_ctl reload". - */ -bool -pg_ctl_reload(const char *pg_ctl, const char *pgdata) -{ - Program program = run_program(pg_ctl, "-D", pgdata, "reload", NULL); - int returnCode = program.returnCode; - - if (program.stdErr != NULL) - { - log_debug("%s", program.stdErr); - } - - free_program(&program); - - if (returnCode != 0) - { - log_error("pg_ctl reload -D %s failed (exit %d)", pgdata, returnCode); - return false; - } - - return true; -} - - -/* - * pg_ctl_promote promotes a standby by running "pg_ctl promote" - */ -bool -pg_ctl_promote(const char *pg_ctl, const char *pgdata) -{ - Program program = - run_program(pg_ctl, "-D", pgdata, "--no-wait", "promote", NULL); - int returnCode = program.returnCode; - - log_debug("%s promote -D %s --no-wait", pg_ctl, pgdata); - - if (program.stdErr != NULL) - { - log_error("%s", program.stdErr); - } - - if (returnCode != 0) - { - /* pg_ctl promote will have logged errors */ - free_program(&program); - return false; - } - - free_program(&program); - return true; -} - - -/* - * pg_setup_standby_mode sets up standby mode by either writing a recovery.conf - * file or adding the configuration items to postgresql.conf and then creating - * a standby.signal file in PGDATA. - */ -bool -pg_setup_standby_mode(uint32_t pg_control_version, - const char *pgdata, - const char *pg_ctl, - ReplicationSource *replicationSource) -{ - if (pg_control_version < 1000) - { - log_fatal("pg_auto_failover does not support PostgreSQL before " - "Postgres 10, we have pg_control version number %d from " - "pg_controldata \"%s\"", - pg_control_version, pgdata); - return false; - } - - /* - * Check our primary_conninfo connection string by attempting to connect in - * replication mode and issuing a IDENTIFY_SYSTEM command. - */ - if (!IS_EMPTY_STRING_BUFFER(replicationSource->primaryNode.host) && - !pgctl_identify_system(replicationSource)) - { - log_error("Failed to setup standby mode: can't connect to the primary. " - "See above for details"); - return false; - } - - if (pg_control_version < 1200) - { - /* - * Before Postgres 12 we used to place recovery configuration in a - * specific file recovery.conf, located alongside postgresql.conf. - * Controling whether the server would start in PITR or standby mode - * was controlled by a setting in the recovery.conf file. - */ - return pg_write_recovery_conf(pgdata, replicationSource); - } - else - { - /* - * Starting in Postgres 12 we need to add our recovery configuration to - * the main postgresql.conf file and create an empty standby.signal - * file to trigger starting the server in standby mode. - */ - return pg_write_standby_signal(pgdata, replicationSource); - } -} - - -/* - * pg_write_recovery_conf writes a recovery.conf file to a postgres data - * directory with the given primary connection info and replication slot name. - */ -static bool -pg_write_recovery_conf(const char *pgdata, ReplicationSource *replicationSource) -{ - char recoveryConfPath[MAXPGPATH]; - - /* prepare storage areas for parameters */ - char primaryConnInfo[MAXCONNINFO] = { 0 }; - char primarySlotName[MAXCONNINFO] = { 0 }; - char targetLSN[PG_LSN_MAXLENGTH] = { 0 }; - char targetAction[NAMEDATALEN] = { 0 }; - char targetTimeline[NAMEDATALEN] = { 0 }; - - GUC recoverySettingsStandby[] = { - { "standby_mode", "'on'" }, - { "primary_conninfo", (char *) primaryConnInfo }, - { "primary_slot_name", (char *) primarySlotName }, - { "recovery_target_timeline", (char *) targetTimeline }, - { NULL, NULL } - }; - - GUC recoverySettingsTargetLSN[] = { - { "standby_mode", "'on'" }, - { "primary_conninfo", (char *) primaryConnInfo }, - { "primary_slot_name", (char *) primarySlotName }, - { "recovery_target_timeline", (char *) targetTimeline }, - { "recovery_target_lsn", (char *) targetLSN }, - { "recovery_target_inclusive", "'true'" }, - { "recovery_target_action", (char *) targetAction }, - { NULL, NULL } - }; - - GUC *recoverySettings = - IS_EMPTY_STRING_BUFFER(replicationSource->targetLSN) - ? recoverySettingsStandby - : recoverySettingsTargetLSN; - - bool includeTuning = false; - - join_path_components(recoveryConfPath, pgdata, "recovery.conf"); - - log_info("Writing recovery configuration to \"%s\"", recoveryConfPath); - - if (!prepare_recovery_settings(pgdata, - replicationSource, - primaryConnInfo, - primarySlotName, - targetLSN, - targetAction, - targetTimeline)) - { - /* errors have already been logged */ - return false; - } - - return ensure_default_settings_file_exists(recoveryConfPath, - recoverySettings, - NULL, - NULL, - includeTuning); -} - - -/* - * pg_write_standby_signal writes the ${PGDATA}/standby.signal file that is in - * use starting with Postgres 12 for starting a standby server. The file only - * needs to exists, and the setup is to be found in the main Postgres - * configuration file. - */ -static bool -pg_write_standby_signal(const char *pgdata, - ReplicationSource *replicationSource) -{ - char standbyConfigFilePath[MAXPGPATH] = { 0 }; - char signalFilePath[MAXPGPATH] = { 0 }; - char configFilePath[MAXPGPATH] = { 0 }; - - /* prepare storage areas for parameters */ - char primaryConnInfo[MAXCONNINFO] = { 0 }; - char primarySlotName[MAXCONNINFO] = { 0 }; - char targetLSN[PG_LSN_MAXLENGTH] = { 0 }; - char targetAction[NAMEDATALEN] = { 0 }; - char targetTimeline[NAMEDATALEN] = { 0 }; - - GUC recoverySettingsStandby[] = { - { "primary_conninfo", (char *) primaryConnInfo }, - { "primary_slot_name", (char *) primarySlotName }, - { "recovery_target_timeline", (char *) targetTimeline }, - { NULL, NULL } - }; - - GUC recoverySettingsTargetLSN[] = { - { "primary_conninfo", (char *) primaryConnInfo }, - { "primary_slot_name", (char *) primarySlotName }, - { "recovery_target_timeline", (char *) targetTimeline }, - { "recovery_target_lsn", (char *) targetLSN }, - { "recovery_target_inclusive", "'true'" }, - { "recovery_target_action", targetAction }, - { NULL, NULL } - }; - - GUC *recoverySettings = - IS_EMPTY_STRING_BUFFER(replicationSource->targetLSN) - ? recoverySettingsStandby - : recoverySettingsTargetLSN; - - bool includeTuning = false; - - log_trace("pg_write_standby_signal"); - - if (!prepare_recovery_settings(pgdata, - replicationSource, - primaryConnInfo, - primarySlotName, - targetLSN, - targetAction, - targetTimeline)) - { - /* errors have already been logged */ - return false; - } - - /* set our configuration file paths, all found in PGDATA */ - join_path_components(signalFilePath, pgdata, "standby.signal"); - join_path_components(configFilePath, pgdata, "postgresql.conf"); - join_path_components(standbyConfigFilePath, - pgdata, - AUTOCTL_STANDBY_CONF_FILENAME); - - /* - * First install the standby.signal file, so that if there's a problem - * later and Postgres is started, it is started as a standby, with missing - * configuration. - */ - - /* only logs about this the first time */ - if (!file_exists(signalFilePath)) - { - log_info("Creating the standby signal file at \"%s\", " - "and replication setup at \"%s\"", - signalFilePath, standbyConfigFilePath); - } - - if (!write_file("", 0, signalFilePath)) - { - /* write_file logs I/O error */ - return false; - } - - /* - * Now write the standby settings to postgresql-auto-failover-standby.conf - * and include that file from postgresql.conf. - * - * we pass NULL as pgSetup because we know it won't be used... - */ - if (!ensure_default_settings_file_exists(standbyConfigFilePath, - recoverySettings, - NULL, - NULL, - includeTuning)) - { - return false; - } - - /* - * We successfully created the standby.signal file, so Postgres will start - * as a standby. If we fail to install the standby settings, then we return - * false here and let the main loop try again. At least Postgres won't - * start as a cloned single accepting writes. - */ - if (!pg_include_config(configFilePath, - AUTOCTL_SB_CONF_INCLUDE_LINE, - AUTOCTL_CONF_INCLUDE_COMMENT)) - { - log_error("Failed to prepare \"%s\" with standby settings", - standbyConfigFilePath); - return false; - } - - return true; -} - - -/* - * prepare_recovery_settings prepares the settings that we need to install in - * either recovery.conf or our own postgresql-auto-failover-standby.conf - * depending on the Postgres major version. - */ -static bool -prepare_recovery_settings(const char *pgdata, - ReplicationSource *replicationSource, - char *primaryConnInfo, - char *primarySlotName, - char *targetLSN, - char *targetAction, - char *targetTimeline) -{ - bool escape = true; - NodeAddress *primaryNode = &(replicationSource->primaryNode); - - /* when reaching REPORT_LSN we set recovery with no primary conninfo */ - if (!IS_EMPTY_STRING_BUFFER(primaryNode->host)) - { - log_debug("prepare_recovery_settings: " - "primary node %" PRId64 " \"%s\" (%s:%d)", - primaryNode->nodeId, - primaryNode->name, - primaryNode->host, - primaryNode->port); - - if (!prepare_primary_conninfo(primaryConnInfo, - MAXCONNINFO, - primaryNode->host, - primaryNode->port, - replicationSource->userName, - NULL, /* no database */ - replicationSource->password, - replicationSource->applicationName, - replicationSource->sslOptions, - escape)) - { - /* errors have already been logged. */ - return false; - } - } - else - { - log_debug("prepare_recovery_settings: no primary node!"); - } - - /* - * We don't always have a replication slot name to use when connecting to a - * standby node. - */ - if (!IS_EMPTY_STRING_BUFFER(replicationSource->slotName)) - { - sformat(primarySlotName, MAXCONNINFO, "'%s'", - replicationSource->slotName); - } - - /* The default target timeline is 'latest' */ - if (IS_EMPTY_STRING_BUFFER(replicationSource->targetTimeline)) - { - sformat(targetTimeline, NAMEDATALEN, "'latest'"); - } - else - { - sformat(targetTimeline, NAMEDATALEN, "'%s'", - replicationSource->targetTimeline); - } - - /* We use the targetLSN only when doing a WAL fast_forward operation */ - if (!IS_EMPTY_STRING_BUFFER(replicationSource->targetLSN)) - { - sformat(targetLSN, PG_LSN_MAXLENGTH, "'%s'", - replicationSource->targetLSN); - } - - /* The default target Action is 'pause' */ - if (IS_EMPTY_STRING_BUFFER(replicationSource->targetAction)) - { - sformat(targetAction, NAMEDATALEN, "'pause'"); - } - else - { - sformat(targetAction, NAMEDATALEN, "'%s'", - replicationSource->targetAction); - } - - return true; -} - - -/* - * pg_cleanup_standby_mode cleans-up the replication settings for the local - * instance of Postgres found at pgdata. - * - * - remove either recovery.conf or standby.signal - * - * - when using Postgres 12 also make postgresql-auto-failover-standby.conf an - * empty file, so that we can still include it, but it has no effect. - */ -bool -pg_cleanup_standby_mode(uint32_t pg_control_version, - const char *pg_ctl, - const char *pgdata, - PGSQL *pgsql) -{ - if (pg_control_version < 1200) - { - char recoveryConfPath[MAXPGPATH]; - - join_path_components(recoveryConfPath, pgdata, "recovery.conf"); - - log_debug("pg_cleanup_standby_mode: rm \"%s\"", recoveryConfPath); - - if (!unlink_file(recoveryConfPath)) - { - /* errors have already been logged */ - return false; - } - } - else - { - char standbyConfigFilePath[MAXPGPATH]; - char signalFilePath[MAXPGPATH]; - - join_path_components(signalFilePath, pgdata, "standby.signal"); - join_path_components(standbyConfigFilePath, - pgdata, - AUTOCTL_STANDBY_CONF_FILENAME); - - log_debug("pg_cleanup_standby_mode: rm \"%s\"", signalFilePath); - - if (!unlink_file(signalFilePath)) - { - /* errors have already been logged */ - return false; - } - - /* empty out the standby configuration file */ - log_debug("pg_cleanup_standby_mode: > \"%s\"", standbyConfigFilePath); - - if (!write_file("", 0, standbyConfigFilePath)) - { - /* write_file logs I/O error */ - return false; - } - } - - return true; -} - - -/* - * escape_recovery_conf_string escapes a string that is used in a recovery.conf - * file by converting single quotes into two single quotes. - * - * The result is written to destination and the length of the result. - */ -static bool -escape_recovery_conf_string(char *destination, int destinationSize, - const char *recoveryConfString) -{ - int charIndex = 0; - int length = strlen(recoveryConfString); - int escapedStringLength = 0; - - /* we are going to add at least 3 chars: two quotes and a NUL character */ - if (destinationSize < (length + 3)) - { - log_error("BUG: failed to escape recovery parameter value \"%s\" " - "in a buffer of %d bytes", - recoveryConfString, destinationSize); - return false; - } - - destination[escapedStringLength++] = '\''; - - for (charIndex = 0; charIndex < length; charIndex++) - { - char currentChar = recoveryConfString[charIndex]; - - if (currentChar == '\'') - { - destination[escapedStringLength++] = '\''; - if (destinationSize < escapedStringLength) - { - log_error( - "BUG: failed to escape recovery parameter value \"%s\" " - "in a buffer of %d bytes, stopped at index %d", - recoveryConfString, destinationSize, charIndex); - return false; - } - } - - destination[escapedStringLength++] = currentChar; - if (destinationSize < escapedStringLength) - { - log_error("BUG: failed to escape recovery parameter value \"%s\" " - "in a buffer of %d bytes, stopped at index %d", - recoveryConfString, destinationSize, charIndex); - return false; - } - } - - destination[escapedStringLength++] = '\''; - destination[escapedStringLength] = '\0'; - - return true; -} - - -/* - * prepare_primary_conninfo prepares a connection string to the primary server. - * The connection string may be used unquoted in a command line calling either - * pg_basebackup ro pg_rewind, or may be used quoted in the primary_conninfo - * setting for PostgreSQL. - * - * Also, pg_rewind needs a database to connect to. - */ -static bool -prepare_primary_conninfo(char *primaryConnInfo, - int primaryConnInfoSize, - const char *primaryHost, - int primaryPort, - const char *replicationUsername, - const char *dbname, - const char *replicationPassword, - const char *applicationName, - SSLOptions sslOptions, - bool escape) -{ - int size = 0; - char escaped[BUFSIZE]; - - if (IS_EMPTY_STRING_BUFFER(primaryHost)) - { - log_debug("prepare_primary_conninfo: missing primary hostname"); - - bzero((void *) primaryConnInfo, primaryConnInfoSize); - - return true; - } - - PQExpBuffer buffer = createPQExpBuffer(); - - if (buffer == NULL) - { - log_error("Failed to allocate memory"); - return false; - } - - /* application_name shows up in pg_stat_replication on the primary */ - appendPQExpBuffer(buffer, "application_name=%s", applicationName); - appendPQExpBuffer(buffer, " host=%s", primaryHost); - appendPQExpBuffer(buffer, " port=%d", primaryPort); - appendPQExpBuffer(buffer, " user=%s", replicationUsername); - - if (dbname != NULL) - { - appendPQExpBuffer(buffer, " dbname=%s", dbname); - } - - if (replicationPassword != NULL && !IS_EMPTY_STRING_BUFFER(replicationPassword)) - { - appendPQExpBuffer(buffer, " password=%s", replicationPassword); - } - - appendPQExpBufferStr(buffer, " "); - if (!prepare_conninfo_sslmode(buffer, sslOptions)) - { - /* errors have already been logged */ - destroyPQExpBuffer(buffer); - return false; - } - - /* memory allocation could have failed while building string */ - if (PQExpBufferBroken(buffer)) - { - log_error("Failed to allocate memory"); - destroyPQExpBuffer(buffer); - return false; - } - - if (escape) - { - if (!escape_recovery_conf_string(escaped, BUFSIZE, buffer->data)) - { - /* errors have already been logged. */ - destroyPQExpBuffer(buffer); - return false; - } - - /* now copy the buffer into primaryConnInfo for the caller */ - size = sformat(primaryConnInfo, primaryConnInfoSize, "%s", escaped); - - if (size == -1 || size > primaryConnInfoSize) - { - log_error("BUG: the escaped primary_conninfo requires %d bytes and " - "pg_auto_failover only support up to %d bytes", - size, primaryConnInfoSize); - destroyPQExpBuffer(buffer); - return false; - } - } - else - { - strlcpy(primaryConnInfo, buffer->data, primaryConnInfoSize); - } - - destroyPQExpBuffer(buffer); - - return true; -} - - -/* - * prepare_conninfo_sslmode adds the sslmode setting to the buffer, which is - * used as a connection string. - */ -static bool -prepare_conninfo_sslmode(PQExpBuffer buffer, SSLOptions sslOptions) -{ - if (sslOptions.sslMode == SSL_MODE_UNKNOWN) - { - if (sslOptions.active) - { - /* that's a bug really */ - log_error("SSL is active in the configuration, " - "but sslmode is unknown"); - return false; - } - - return true; - } - - appendPQExpBuffer(buffer, "sslmode=%s", - pgsetup_sslmode_to_string(sslOptions.sslMode)); - - if (sslOptions.sslMode >= SSL_MODE_VERIFY_CA) - { - /* ssl revocation list might not be provided, it's ok */ - if (!IS_EMPTY_STRING_BUFFER(sslOptions.crlFile)) - { - appendPQExpBuffer(buffer, " sslrootcert=%s sslcrl=%s", - sslOptions.caFile, sslOptions.crlFile); - } - else - { - appendPQExpBuffer(buffer, " sslrootcert=%s", sslOptions.caFile); - } - } - - return true; -} - - -/* - * pgctl_identify_system connects with replication=1 to our target node and run - * the IDENTIFY_SYSTEM command to check that HBA is ready. - */ -bool -pgctl_identify_system(ReplicationSource *replicationSource) -{ - NodeAddress *primaryNode = &(replicationSource->primaryNode); - - char primaryConnInfo[MAXCONNINFO] = { 0 }; - char primaryConnInfoReplication[MAXCONNINFO] = { 0 }; - PGSQL replicationClient = { 0 }; - - if (!prepare_primary_conninfo(primaryConnInfo, - MAXCONNINFO, - primaryNode->host, - primaryNode->port, - replicationSource->userName, - NULL, /* no database */ - replicationSource->password, - replicationSource->applicationName, - replicationSource->sslOptions, - false)) /* no need for escaping */ - { - /* errors have already been logged. */ - return false; - } - - /* - * Per https://www.postgresql.org/docs/12/protocol-replication.html: - * - * To initiate streaming replication, the frontend sends the replication - * parameter in the startup message. A Boolean value of true (or on, yes, - * 1) tells the backend to go into physical replication walsender mode, - * wherein a small set of replication commands, shown below, can be issued - * instead of SQL statements. - */ - int len = sformat(primaryConnInfoReplication, MAXCONNINFO, - "%s replication=1", - primaryConnInfo); - - if (len >= MAXCONNINFO) - { - log_warn("Failed to call IDENTIFY_SYSTEM: primary_conninfo too large"); - return false; - } - - if (!pgsql_init(&replicationClient, - primaryConnInfoReplication, - PGSQL_CONN_UPSTREAM)) - { - /* errors have already been logged */ - return false; - } - - if (!pgsql_identify_system(&replicationClient, - &(replicationSource->system))) - { - /* errors have already been logged */ - return false; - } - - return true; -} - - -/* - * pg_is_running returns true if PostgreSQL is running. - */ -bool -pg_is_running(const char *pg_ctl, const char *pgdata) -{ - return pg_ctl_status(pg_ctl, pgdata, false) == 0; -} - - -/* - * pg_create_self_signed_cert creates self-signed certificates for the local - * Postgres server and places the private key in $PGDATA/server.key and the - * public certificate in $PGDATA/server.cert - * - * We simply follow Postgres documentation at: - * https://www.postgresql.org/docs/current/ssl-tcp.html#SSL-CERTIFICATE-CREATION - * - * openssl req -new -x509 -days 365 -nodes -text -out server.crt \ - * -keyout server.key -subj "/CN=dbhost.yourdomain.com" - */ -bool -pg_create_self_signed_cert(PostgresSetup *pgSetup, const char *hostname) -{ - char subject[BUFSIZE] = { 0 }; - char openssl[MAXPGPATH] = { 0 }; - - if (!search_path_first("openssl", openssl, LOG_ERROR)) - { - /* errors have already been logged */ - return false; - } - - /* ensure PGDATA has been normalized */ - if (!normalize_filename(pgSetup->pgdata, pgSetup->pgdata, MAXPGPATH)) - { - return false; - } - - int size = sformat(pgSetup->ssl.serverKey, MAXPGPATH, - "%s/server.key", pgSetup->pgdata); - - if (size == -1 || size > MAXPGPATH) - { - log_error("BUG: the ssl server key file path requires %d bytes and " - "pg_auto_failover only support up to %d bytes", - size, MAXPGPATH); - return false; - } - - size = sformat(pgSetup->ssl.serverCert, MAXPGPATH, - "%s/server.crt", pgSetup->pgdata); - - if (size == -1 || size > MAXPGPATH) - { - log_error("BUG: the ssl server key file path requires %d bytes and " - "pg_auto_failover only support up to %d bytes", - size, MAXPGPATH); - return false; - } - - size = sformat(subject, BUFSIZE, "/CN=%s", hostname); - - if (size == -1 || size > BUFSIZE) - { - log_error("BUG: the ssl subject \"/CN=%s\" requires %d bytes and" - "pg_auto_failover only support up to %d bytes", - hostname, size, BUFSIZE); - return false; - } - - log_info(" %s req -new -x509 -days 365 -nodes -text " - "-out %s -keyout %s -subj \"%s\"", - openssl, - pgSetup->ssl.serverCert, - pgSetup->ssl.serverKey, - subject); - - Program program = run_program(openssl, - "req", "-new", "-x509", "-days", "365", - "-nodes", "-text", - "-out", pgSetup->ssl.serverCert, - "-keyout", pgSetup->ssl.serverKey, - "-subj", subject, - NULL); - - if (program.returnCode != 0) - { - (void) log_program_output(program, LOG_INFO, LOG_ERROR); - log_error("openssl failed with return code: %d", program.returnCode); - free_program(&program); - return false; - } - - (void) log_program_output(program, LOG_DEBUG, LOG_DEBUG); - free_program(&program); - - /* - * Then do: chmod og-rwx server.key - */ - if (chmod(pgSetup->ssl.serverKey, S_IRUSR | S_IWUSR) != 0) - { - log_error("Failed to chmod og-rwx \"%s\": %m", pgSetup->ssl.serverKey); - return false; - } - - return true; -} diff --git a/src/bin/pg_autoctl/pgctl.h b/src/bin/pg_autoctl/pgctl.h deleted file mode 100644 index 3466c47a6..000000000 --- a/src/bin/pg_autoctl/pgctl.h +++ /dev/null @@ -1,79 +0,0 @@ -/* - * src/bin/pg_autoctl/pgctl.h - * API for controling PostgreSQL, using its binary tooling (pg_ctl, - * pg_controldata, pg_basebackup and such). - * - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the PostgreSQL License. - * - */ - -#ifndef PGCTL_H -#define PGCTL_H - -#include -#include -#include - -#include "postgres_fe.h" -#include "utils/pidfile.h" - -#include "defaults.h" -#include "file_utils.h" -#include "pgsetup.h" -#include "pgsql.h" - -#define AUTOCTL_DEFAULTS_CONF_FILENAME "postgresql-auto-failover.conf" -#define AUTOCTL_STANDBY_CONF_FILENAME "postgresql-auto-failover-standby.conf" - -#define PG_CTL_STATUS_NOT_RUNNING 3 - -bool pg_controldata(PostgresSetup *pgSetup, bool missing_ok); -bool set_pg_ctl_from_PG_CONFIG(PostgresSetup *pgSetup); -bool set_pg_ctl_from_pg_config(PostgresSetup *pgSetup); -bool config_find_pg_ctl(PostgresSetup *pgSetup); -bool find_extension_control_file(const char *pg_ctl, const char *extName); -bool pg_ctl_version(PostgresSetup *pgSetup); -bool set_pg_ctl_from_config_bindir(PostgresSetup *pgSetup, const char *pg_config); -bool find_pg_config_from_pg_ctl(const char *pg_ctl, char *pg_config, size_t size); - -bool pg_add_auto_failover_default_settings(PostgresSetup *pgSetup, - const char *hostname, - const char *configFilePath, - GUC *settings); - -bool pg_auto_failover_default_settings_file_exists(PostgresSetup *pgSetup); - -bool pg_basebackup(const char *pgdata, - const char *pg_ctl, - ReplicationSource *replicationSource); -bool pg_rewind(const char *pgdata, - const char *pg_ctl, - ReplicationSource *replicationSource); - -bool pg_ctl_initdb(const char *pg_ctl, const char *pgdata); -bool pg_ctl_postgres(const char *pg_ctl, const char *pgdata, int pgport, - char *listen_addresses, bool listen); -bool pg_log_startup(const char *pgdata, int logLevel); -bool pg_log_recovery_setup(const char *pgdata, int logLevel); -bool pg_ctl_stop(const char *pg_ctl, const char *pgdata); -bool pg_ctl_reload(const char *pg_ctl, const char *pgdata); -int pg_ctl_status(const char *pg_ctl, const char *pgdata, bool log_output); -bool pg_ctl_promote(const char *pg_ctl, const char *pgdata); - -bool pg_setup_standby_mode(uint32_t pg_control_version, - const char *pgdata, - const char *pg_ctl, - ReplicationSource *replicationSource); - -bool pg_cleanup_standby_mode(uint32_t pg_control_version, - const char *pg_ctl, - const char *pgdata, - PGSQL *pgsql); - -bool pgctl_identify_system(ReplicationSource *replicationSource); - -bool pg_is_running(const char *pg_ctl, const char *pgdata); -bool pg_create_self_signed_cert(PostgresSetup *pgSetup, const char *hostname); - -#endif /* PGCTL_H */ diff --git a/src/bin/pg_autoctl/pgsetup.c b/src/bin/pg_autoctl/pgsetup.c deleted file mode 100644 index 5554d0344..000000000 --- a/src/bin/pg_autoctl/pgsetup.c +++ /dev/null @@ -1,2039 +0,0 @@ -/* - * src/bin/pg_autoctl/pgsetup.c - * Discovers a PostgreSQL setup by calling pg_controldata and reading - * postmaster.pid file, getting clues from the process environment and from - * user given hints (options). - * - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the PostgreSQL License. - * - */ - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "parson.h" - -#include "postgres_fe.h" -#include "pqexpbuffer.h" - -#include "defaults.h" -#include "env_utils.h" -#include "log.h" -#include "parsing.h" -#include "pgctl.h" -#include "signals.h" -#include "string_utils.h" - - -static bool get_pgpid(PostgresSetup *pgSetup, bool pgIsNotRunningIsOk); -static PostmasterStatus pmStatusFromString(const char *postmasterStatus); - - -/* - * Discover PostgreSQL environment from given clues, or a partial setup. - * - * This routines check the PATH for pg_ctl, and is ok when there's a single - * entry in found. It then uses either given PGDATA or the environment value - * and runs a pg_controldata to get system identifier and PostgreSQL version - * numbers. Then it reads PGDATA/postmaster.pid to get the pid and the port of - * the running PostgreSQL server. Then it can connects to it and see if it's in - * recovery. - */ -bool -pg_setup_init(PostgresSetup *pgSetup, - PostgresSetup *options, - bool missing_pgdata_is_ok, - bool pg_is_not_running_is_ok) -{ - int errors = 0; - - /* - * Make sure that we keep the options->nodeKind in the pgSetup. - */ - pgSetup->pgKind = options->pgKind; - - /* - * Also make sure that we keep the pg_controldata results if we have them. - */ - pgSetup->control = options->control; - - /* - * Also make sure that we keep the hbaLevel to edit. Remember that - * --skip-pg-hba is registered in the config as --auth skip. - */ - if (strcmp(options->authMethod, "skip") == 0) - { - pgSetup->hbaLevel = HBA_EDIT_SKIP; - strlcpy(pgSetup->hbaLevelStr, options->authMethod, NAMEDATALEN); - } - else - { - pgSetup->hbaLevel = options->hbaLevel; - strlcpy(pgSetup->hbaLevelStr, options->hbaLevelStr, NAMEDATALEN); - } - - /* - * Make sure that we keep the SSL options too. - */ - pgSetup->ssl.active = options->ssl.active; - pgSetup->ssl.createSelfSignedCert = options->ssl.createSelfSignedCert; - pgSetup->ssl.sslMode = options->ssl.sslMode; - strlcpy(pgSetup->ssl.sslModeStr, options->ssl.sslModeStr, SSL_MODE_STRLEN); - strlcpy(pgSetup->ssl.caFile, options->ssl.caFile, MAXPGPATH); - strlcpy(pgSetup->ssl.crlFile, options->ssl.crlFile, MAXPGPATH); - strlcpy(pgSetup->ssl.serverCert, options->ssl.serverCert, MAXPGPATH); - strlcpy(pgSetup->ssl.serverKey, options->ssl.serverKey, MAXPGPATH); - - /* Also make sure we keep the citus specific clusterName option */ - strlcpy(pgSetup->citusClusterName, options->citusClusterName, NAMEDATALEN); - - /* check or find pg_ctl, unless we already have it */ - if (IS_EMPTY_STRING_BUFFER(pgSetup->pg_ctl) || - IS_EMPTY_STRING_BUFFER(pgSetup->pg_version)) - { - if (!IS_EMPTY_STRING_BUFFER(options->pg_ctl)) - { - /* copy over pg_ctl and pg_version */ - strlcpy(pgSetup->pg_ctl, options->pg_ctl, MAXPGPATH); - strlcpy(pgSetup->pg_version, options->pg_version, - PG_VERSION_STRING_MAX); - - /* we might not have fetched the version yet */ - if (IS_EMPTY_STRING_BUFFER(pgSetup->pg_version)) - { - /* also cache the version in options */ - if (!pg_ctl_version(options)) - { - /* we already logged about it */ - return false; - } - - strlcpy(pgSetup->pg_version, - options->pg_version, - sizeof(pgSetup->pg_version)); - - log_debug("pg_setup_init: %s version %s", - pgSetup->pg_ctl, pgSetup->pg_version); - } - } - else - { - if (!config_find_pg_ctl(pgSetup)) - { - /* config_find_pg_ctl already logged errors */ - errors++; - } - } - } - - /* check or find PGDATA */ - if (options->pgdata[0] != '\0') - { - strlcpy(pgSetup->pgdata, options->pgdata, MAXPGPATH); - } - else - { - if (!get_env_pgdata(pgSetup->pgdata)) - { - log_error("Failed to set PGDATA either from the environment " - "or from --pgdata"); - errors++; - } - } - - if (!missing_pgdata_is_ok && !directory_exists(pgSetup->pgdata)) - { - log_fatal("Database directory \"%s\" not found", pgSetup->pgdata); - return false; - } - else if (!missing_pgdata_is_ok) - { - char globalControlPath[MAXPGPATH] = { 0 }; - - /* globalControlFilePath = $PGDATA/global/pg_control */ - join_path_components(globalControlPath, - pgSetup->pgdata, "global/pg_control"); - - if (!file_exists(globalControlPath)) - { - log_error("PGDATA exists but is not a Postgres directory, " - "see above for details"); - return false; - } - } - - /* get the real path of PGDATA now */ - if (directory_exists(pgSetup->pgdata)) - { - if (!normalize_filename(pgSetup->pgdata, pgSetup->pgdata, MAXPGPATH)) - { - /* errors have already been logged */ - return false; - } - } - - /* check of find username */ - if (options->username[0] != '\0') - { - strlcpy(pgSetup->username, options->username, NAMEDATALEN); - } - else - { - /* - * If a PGUSER environment variable is defined, take the value from - * there. Otherwise we attempt to connect without username. In that - * case the username will be determined based on the current user. - */ - if (!get_env_copy_with_fallback("PGUSER", pgSetup->username, NAMEDATALEN, "")) - { - /* errors have already been logged */ - return false; - } - } - - /* check or find dbname */ - if (!IS_EMPTY_STRING_BUFFER(options->dbname)) - { - strlcpy(pgSetup->dbname, options->dbname, NAMEDATALEN); - } - else - { - /* - * If a PGDATABASE environment variable is defined, take the value from - * there. Otherwise we attempt to connect without a database name, and - * the default will use the username here instead. - */ - if (!get_env_copy_with_fallback("PGDATABASE", pgSetup->dbname, NAMEDATALEN, - DEFAULT_DATABASE_NAME)) - { - /* errors have already been logged */ - return false; - } - } - - /* - * Read the postmaster.pid file to find out pid, port and unix socket - * directory of a running PostgreSQL instance. - */ - bool pgIsReady = pg_setup_is_ready(pgSetup, pg_is_not_running_is_ok); - - if (!pgIsReady && !pg_is_not_running_is_ok) - { - /* errors have already been logged */ - errors++; - } - - /* - * check or find PGHOST - * - * By order of preference, we use: - * --pghost command line option - * PGDATA/postmaster.pid - * PGHOST from the environment - */ - if (options->pghost[0] != '\0') - { - strlcpy(pgSetup->pghost, options->pghost, _POSIX_HOST_NAME_MAX); - } - else - { - /* read_pg_pidfile might already have set pghost for us */ - if (pgSetup->pghost[0] == '\0') - { - /* - * We can (at least try to) connect without host= in the connection - * string, so missing PGHOST and --pghost isn't an error. - */ - if (!get_env_copy_with_fallback("PGHOST", pgSetup->pghost, - _POSIX_HOST_NAME_MAX, "")) - { - /* errors have already been logged */ - return false; - } - } - } - - /* - * In test environment we might disable unix socket directories. In that - * case, we need to have an host to connect to, accepting to connect - * without host= in the connection string is not going to cut it. - */ - if (IS_EMPTY_STRING_BUFFER(pgSetup->pghost)) - { - if (env_found_empty("PG_REGRESS_SOCK_DIR")) - { - log_error("PG_REGRESS_SOCK_DIR is set to \"\" to disable unix " - "socket directories, now --pghost is mandatory, " - "but unset."); - errors++; - } - } - - /* check or find PGPORT - * - * By order or preference, we use: - * --pgport command line option - * PGDATA/postmaster.pid - * PGPORT from the environment - * POSTGRES_PORT from our hard coded defaults (5432, see defaults.h) - */ - if (options->pgport > 0) - { - pgSetup->pgport = options->pgport; - } - else - { - /* if we have a running cluster, just use its port */ - if (pgSetup->pidFile.pid > 0 && pgSetup->pidFile.port > 0) - { - pgSetup->pgport = pgSetup->pidFile.port; - } - else - { - /* - * no running cluster, what about using PGPORT then? - */ - pgSetup->pgport = pgsetup_get_pgport(); - } - } - - /* Set proxy port */ - if (options->proxyport > 0) - { - pgSetup->proxyport = options->proxyport; - } - - - /* - * If --listen is given, then set our listen_addresses to this value - */ - if (!IS_EMPTY_STRING_BUFFER(options->listen_addresses)) - { - strlcpy(pgSetup->listen_addresses, - options->listen_addresses, MAXPGPATH); - } - else - { - /* - * The default listen_addresses is '*', because we are dealing with a - * cluster setup and 'localhost' isn't going to cut it: the monitor and - * the coordinator nodes need to be able to connect to our local node - * using a connection string with hostname:port. - */ - strlcpy(pgSetup->listen_addresses, - POSTGRES_DEFAULT_LISTEN_ADDRESSES, MAXPGPATH); - } - - - /* - * If --auth is given, then set our authMethod to this value - * otherwise it remains empty - */ - if (!IS_EMPTY_STRING_BUFFER(options->authMethod)) - { - strlcpy(pgSetup->authMethod, - options->authMethod, NAMEDATALEN); - } - - pgSetup->settings = options->settings; - - /* - * And we always double-check with PGDATA/postmaster.pid if we have it, and - * we should have it in the normal/expected case. - */ - if (pgIsReady && - pgSetup->pidFile.pid > 0 && - pgSetup->pgport != pgSetup->pidFile.port) - { - log_error("Given --pgport %d doesn't match PostgreSQL " - "port %d from \"%s/postmaster.pid\"", - pgSetup->pgport, pgSetup->pidFile.port, pgSetup->pgdata); - errors++; - } - - /* - * When we have a PGDATA and Postgres is not running, we need to grab more - * information about the local installation: pg_controldata can give us the - * pg-_control_version, catalog_version_no, and system_identifier. - */ - if (errors == 0) - { - /* - * Only run pg_controldata when Postgres is not running, otherwise we - * get the same information later from an SQL query, see - * pgsql_get_postgres_metadata. - */ - if (!pg_setup_is_running(pgSetup) && - pgSetup->control.pg_control_version == 0) - { - pg_controldata(pgSetup, missing_pgdata_is_ok); - - if (pgSetup->control.pg_control_version == 0) - { - /* we already logged about it */ - if (!missing_pgdata_is_ok) - { - errors++; - } - } - else - { - log_debug("Found PostgreSQL system %" PRIu64 " at \"%s\", " - "version %u, catalog version %u", - pgSetup->control.system_identifier, - pgSetup->pgdata, - pgSetup->control.pg_control_version, - pgSetup->control.catalog_version_no); - } - } - } - - /* - * Sometimes `pg_ctl start` returns with success and Postgres is still in - * crash recovery replaying WAL files, in the "starting" state rather than - * the "ready" state. - * - * In that case, we wait until Postgres is ready for connections. The whole - * pg_autoctl code is expecting to be able to connect to Postgres, so - * there's no point in returning now and having the next connection attempt - * fail with something like the following: - * - * ERROR Connection to database failed: FATAL: the database system is - * starting up - */ - if (pgSetup->pidFile.port > 0 && - pgSetup->pgport == pgSetup->pidFile.port) - { - if (!pgIsReady) - { - if (!pg_is_not_running_is_ok) - { - log_error("Failed to read Postgres pidfile, " - "see above for details"); - return false; - } - } - } - - if (errors > 0) - { - log_fatal("Failed to discover PostgreSQL setup, " - "please fix previous errors."); - return false; - } - - return true; -} - - -/* - * Read the first line of the PGDATA/postmaster.pid file to get Postgres PID. - */ -static bool -get_pgpid(PostgresSetup *pgSetup, bool pgIsNotRunningIsOk) -{ - char *contents = NULL; - long fileSize = 0; - char pidfile[MAXPGPATH]; - char *lines[1]; - int pid = -1; - - /* when !pgIsNotRunningIsOk then log_error(), otherwise log_debug() */ - int logLevel = pgIsNotRunningIsOk ? LOG_TRACE : LOG_ERROR; - - join_path_components(pidfile, pgSetup->pgdata, "postmaster.pid"); - - if (!read_file_if_exists(pidfile, &contents, &fileSize)) - { - log_level(logLevel, "Failed to open file \"%s\": %m", pidfile); - - if (!pgIsNotRunningIsOk) - { - log_info("Is PostgreSQL at \"%s\" up and running?", pgSetup->pgdata); - } - return false; - } - - if (fileSize == 0) - { - /* yeah, that happens (race condition, kind of) */ - log_debug("The PID file \"%s\" is empty", pidfile); - free(contents); - return false; - } - else if (splitLines(contents, lines, 1) != 1 || - !stringToInt(lines[0], &pid)) - { - log_warn("Invalid data in PID file \"%s\"", pidfile); - free(contents); - return false; - } - - free(contents); - contents = NULL; - - /* postmaster PID (or negative of a standalone backend's PID) */ - if (pid < 0) - { - int standalonePid = -1 * pid; - - if (kill(standalonePid, 0) == 0) - { - pgSetup->pidFile.pid = pid; - return true; - } - log_debug("Read a stale standalone pid in \"postmaster.pid\": %d", pid); - return false; - } - else if (pid > 0 && pid <= INT_MAX) - { - if (kill(pid, 0) == 0) - { - pgSetup->pidFile.pid = pid; - return true; - } - else - { - logLevel = pgIsNotRunningIsOk ? LOG_DEBUG : LOG_WARN; - - log_level(logLevel, - "Read a stale pid in \"postmaster.pid\": %d", pid); - - return false; - } - } - else - { - /* that's more like a bug, really */ - log_error("Invalid PID \"%d\" read in \"postmaster.pid\"", pid); - return false; - } -} - - -/* - * Read the PGDATA/postmaster.pid file to get the port number of the running - * server we're asked to keep highly available. - */ -bool -read_pg_pidfile(PostgresSetup *pgSetup, bool pgIsNotRunningIsOk, int maxRetries) -{ - FILE *fp; - int lineno; - char line[BUFSIZE]; - char pidfile[MAXPGPATH]; - - join_path_components(pidfile, pgSetup->pgdata, "postmaster.pid"); - - if ((fp = fopen_read_only(pidfile)) == NULL) - { - /* - * Maybe we're attempting to read the file during Postgres start-up - * phase and we just got where the file is replaced, when going from - * standalone backend to full service. - */ - if (maxRetries > 0) - { - log_trace("read_pg_pidfile: \"%s\" does not exist [%d]", - pidfile, maxRetries); - pg_usleep(250 * 1000); /* wait for 250ms and try again */ - return read_pg_pidfile(pgSetup, pgIsNotRunningIsOk, maxRetries - 1); - } - - if (!pgIsNotRunningIsOk) - { - log_error("Failed to open file \"%s\": %m", pidfile); - log_info("Is PostgreSQL at \"%s\" up and running?", pgSetup->pgdata); - } - return false; - } - - for (lineno = 1; lineno <= LOCK_FILE_LINE_PM_STATUS; lineno++) - { - if (fgets(line, sizeof(line), fp) == NULL) - { - /* later lines are added during start-up, will appear later */ - if (lineno > LOCK_FILE_LINE_PORT) - { - /* that's retry-able */ - fclose(fp); - - if (maxRetries == 0) - { - /* partial read is ok, pgSetup keeps track */ - return true; - } - - pg_usleep(250 * 1000); /* sleep for 250ms */ - log_trace("read_pg_pidfile: fgets is NULL for lineno %d, retry %d", - lineno, maxRetries); - return read_pg_pidfile(pgSetup, - pgIsNotRunningIsOk, - maxRetries - 1); - } - else - { - /* don't use %m to print errno, errno is not set by fgets */ - log_error("Failed to read line %d from file \"%s\"", - lineno, pidfile); - fclose(fp); - return false; - } - } - - int lineLength = strlen(line); - - /* chomp the ending Newline (\n) */ - if (lineLength > 0) - { - line[lineLength - 1] = '\0'; - lineLength = strlen(line); - } - - if (lineno == LOCK_FILE_LINE_PID) - { - int pid = 0; - if (!stringToInt(line, &pid)) - { - log_error("Postgres pidfile does not contain a valid pid %s", - line); - - return false; - } - - /* a standalone backend pid is negative, we signal the actual pid */ - pgSetup->pidFile.pid = abs(pid); - - if (kill(pgSetup->pidFile.pid, 0) != 0) - { - log_error("Postgres pidfile contains pid %d, " - "which is not running", pgSetup->pidFile.pid); - - /* well then reset the PID to our unknown value */ - pgSetup->pidFile.pid = 0; - - return false; - } - - if (pid < 0) - { - /* standalone backend during the start-up process */ - break; - } - } - - if (lineno == LOCK_FILE_LINE_PORT) - { - if (!stringToUShort(line, &pgSetup->pidFile.port)) - { - log_error("Postgres pidfile does not contain a valid port %s", - line); - - return false; - } - } - - if (lineno == LOCK_FILE_LINE_SOCKET_DIR) - { - if (lineLength > 0) - { - int n = strlcpy(pgSetup->pghost, line, _POSIX_HOST_NAME_MAX); - - if (n >= _POSIX_HOST_NAME_MAX) - { - log_error("Failed to read unix socket directory \"%s\" " - "from file \"%s\": the directory name is %d " - "characters long, " - "and pg_autoctl only accepts up to %d characters", - line, pidfile, n, _POSIX_HOST_NAME_MAX - 1); - return false; - } - } - } - - if (lineno == LOCK_FILE_LINE_PM_STATUS) - { - if (lineLength > 0) - { - pgSetup->pm_status = pmStatusFromString(line); - } - } - } - fclose(fp); - - log_trace("read_pg_pidfile: pid %d, port %d, host %s, status \"%s\"", - pgSetup->pidFile.pid, - pgSetup->pidFile.port, - pgSetup->pghost, - pmStatusToString(pgSetup->pm_status)); - - return true; -} - - -/* - * fprintf_pg_setup prints to given STREAM the current setting found in - * pgSetup. - */ -void -fprintf_pg_setup(FILE *stream, PostgresSetup *pgSetup) -{ - int pgversion = 0; - - (void) parse_pg_version_string(pgSetup->pg_version, &pgversion); - - fformat(stream, "pgdata: %s\n", pgSetup->pgdata); - fformat(stream, "pg_ctl: %s\n", pgSetup->pg_ctl); - - fformat(stream, "pg_version: \"%s\" (%d)\n", - pgSetup->pg_version, pgversion); - - fformat(stream, "pghost: %s\n", pgSetup->pghost); - fformat(stream, "pgport: %d\n", pgSetup->pgport); - fformat(stream, "proxyport: %d\n", pgSetup->proxyport); - fformat(stream, "pid: %d\n", pgSetup->pidFile.pid); - fformat(stream, "is in recovery: %s\n", - pgSetup->is_in_recovery ? "yes" : "no"); - fformat(stream, "Control cluster state: %s\n", - dbstateToString(pgSetup->control.state)); - fformat(stream, "Control Version: %u\n", - pgSetup->control.pg_control_version); - fformat(stream, "Catalog Version: %u\n", - pgSetup->control.catalog_version_no); - fformat(stream, "System Identifier: %" PRIu64 "\n", - pgSetup->control.system_identifier); - fformat(stream, "Latest checkpoint LSN: %s\n", - pgSetup->control.latestCheckpointLSN); - fformat(stream, "Postmaster status: %s\n", - pmStatusToString(pgSetup->pm_status)); - fflush(stream); -} - - -/* - * pg_setup_as_json copies in the given pre-allocated string the json - * representation of the pgSetup. - */ -bool -pg_setup_as_json(PostgresSetup *pgSetup, JSON_Value *js) -{ - JSON_Object *jsobj = json_value_get_object(js); - char system_identifier[BUFSIZE]; - - json_object_set_string(jsobj, "pgdata", pgSetup->pgdata); - json_object_set_string(jsobj, "pg_ctl", pgSetup->pg_ctl); - json_object_set_string(jsobj, "version", pgSetup->pg_version); - json_object_set_string(jsobj, "host", pgSetup->pghost); - json_object_set_number(jsobj, "port", (double) pgSetup->pgport); - json_object_set_number(jsobj, "proxyport", (double) pgSetup->proxyport); - json_object_set_number(jsobj, "pid", (double) pgSetup->pidFile.pid); - json_object_set_boolean(jsobj, "in_recovery", pgSetup->is_in_recovery); - - json_object_dotset_number(jsobj, - "control.version", - (double) pgSetup->control.pg_control_version); - - json_object_dotset_number(jsobj, - "control.catalog_version", - (double) pgSetup->control.catalog_version_no); - - sformat(system_identifier, BUFSIZE, "%" PRIu64, - pgSetup->control.system_identifier); - json_object_dotset_string(jsobj, - "control.system_identifier", - system_identifier); - - json_object_dotset_string(jsobj, - "postmaster.status", - pmStatusToString(pgSetup->pm_status)); - return true; -} - - -/* - * pg_setup_get_local_connection_string build a connecting string to connect - * to the local postgres server and writes it to connectionString, which should - * be at least MAXCONNINFO in size. - */ -bool -pg_setup_get_local_connection_string(PostgresSetup *pgSetup, - char *connectionString) -{ - char pg_regress_sock_dir[MAXPGPATH] = { 0 }; - bool pg_regress_sock_dir_exists = env_exists("PG_REGRESS_SOCK_DIR"); - PQExpBuffer connStringBuffer = createPQExpBuffer(); - - if (connStringBuffer == NULL) - { - log_error("Failed to allocate memory"); - return false; - } - - appendPQExpBuffer(connStringBuffer, "port=%d dbname=%s", - pgSetup->pgport, pgSetup->dbname); - - if (pg_regress_sock_dir_exists && - !get_env_copy("PG_REGRESS_SOCK_DIR", pg_regress_sock_dir, MAXPGPATH)) - { - /* errors have already been logged */ - destroyPQExpBuffer(connStringBuffer); - return false; - } - - /* - * When PG_REGRESS_SOCK_DIR is set and empty, we force the connection - * string to use "localhost" (TCP/IP hostname for IP 127.0.0.1 or ::1, - * usually), even when the configuration setup is using a unix directory - * setting. - */ - if (env_found_empty("PG_REGRESS_SOCK_DIR") && - (IS_EMPTY_STRING_BUFFER(pgSetup->pghost) || - pgSetup->pghost[0] == '/')) - { - appendPQExpBufferStr(connStringBuffer, " host=localhost"); - } - else if (!IS_EMPTY_STRING_BUFFER(pgSetup->pghost)) - { - if (pg_regress_sock_dir_exists && strlen(pg_regress_sock_dir) > 0 && - strcmp(pgSetup->pghost, pg_regress_sock_dir) != 0) - { - /* - * It might turn out ok (stray environment), but in case of - * connection error, this warning should be useful to debug the - * situation. - */ - log_warn("PG_REGRESS_SOCK_DIR is set to \"%s\", " - "and our setup is using \"%s\"", - pg_regress_sock_dir, - pgSetup->pghost); - } - appendPQExpBuffer(connStringBuffer, " host=%s", pgSetup->pghost); - } - - if (!IS_EMPTY_STRING_BUFFER(pgSetup->username)) - { - appendPQExpBuffer(connStringBuffer, " user=%s", pgSetup->username); - } - - if (PQExpBufferBroken(connStringBuffer)) - { - log_error("Failed to allocate memory"); - destroyPQExpBuffer(connStringBuffer); - return false; - } - - if (strlcpy(connectionString, - connStringBuffer->data, MAXCONNINFO) >= MAXCONNINFO) - { - log_error("Failed to copy connection string \"%s\" which is %lu bytes " - "long, pg_autoctl only supports connection strings up to " - " %lu bytes", - connStringBuffer->data, - (unsigned long) connStringBuffer->len, - (unsigned long) MAXCONNINFO); - destroyPQExpBuffer(connStringBuffer); - return false; - } - - destroyPQExpBuffer(connStringBuffer); - return true; -} - - -/* - * pg_setup_pgdata_exists returns true when PGDATA exists, hosts a - * global/pg_control file (so that it looks like a Postgres cluster) and when - * the pg_controldata probe was successful. - */ -bool -pg_setup_pgdata_exists(PostgresSetup *pgSetup) -{ - char globalControlPath[MAXPGPATH] = { 0 }; - - /* make sure our cached value in pgSetup still makes sense */ - if (!directory_exists(pgSetup->pgdata)) - { - return false; - } - - /* globalControlFilePath = $PGDATA/global/pg_control */ - join_path_components(globalControlPath, pgSetup->pgdata, "global/pg_control"); - - if (!file_exists(globalControlPath)) - { - return false; - } - - /* - * Now that we know that PGDATA exists, let's grab the system identifier if - * we don't have it already. - */ - if (pgSetup->control.system_identifier == 0) - { - bool missingPgdataIsOk = false; - - /* errors are logged from within pg_controldata */ - (void) pg_controldata(pgSetup, missingPgdataIsOk); - - return pgSetup->control.system_identifier != 0; - } - - return true; -} - - -/* - * pg_setup_pgdata_exists returns true when the pg_controldata probe was - * susccessful. - */ -bool -pg_setup_is_running(PostgresSetup *pgSetup) -{ - bool pgIsNotRunningIsOk = true; - - return pgSetup->pidFile.pid != 0 - - /* if we don't have the PID yet, try reading it now */ - || (get_pgpid(pgSetup, pgIsNotRunningIsOk) && - pgSetup->pidFile.pid > 0); -} - - -/* - * pg_setup_is_ready returns true when the postmaster.pid file has a "ready" - * status in it, which we parse in pgSetup->pm_status. - */ -bool -pg_setup_is_ready(PostgresSetup *pgSetup, bool pgIsNotRunningIsOk) -{ - char globalControlPath[MAXPGPATH] = { 0 }; - - /* globalControlFilePath = $PGDATA/global/pg_control */ - join_path_components(globalControlPath, pgSetup->pgdata, "global/pg_control"); - - if (!file_exists(globalControlPath)) - { - return false; - } - - /* - * Invalidate in-memory Postmaster status cache. - * - * This makes sure we enter the main loop and attempt to read the - * postmaster.pid file at least once: if Postgres was stopped, then the - * file that we've read previously might not exists anymore. - */ - pgSetup->pm_status = POSTMASTER_STATUS_UNKNOWN; - - /* - * Sometimes `pg_ctl start` returns with success and Postgres is still - * in crash recovery replaying WAL files, in the "starting" state - * rather than the "ready" state. - * - * In that case, we wait until Postgres is ready for connections. The - * whole pg_autoctl code is expecting to be able to connect to - * Postgres, so there's no point in returning now and having the next - * connection attempt fail with something like the following: - * - * ERROR Connection to database failed: FATAL: the database system is - * starting up - */ - while (pgSetup->pm_status != POSTMASTER_STATUS_READY) - { - int maxRetries = 5; - - if (!get_pgpid(pgSetup, pgIsNotRunningIsOk)) - { - /* - * We failed to read the Postgres pid file, and infinite - * looping might not help here anymore. Better give control - * back to the launching process (might be init scripts, - * systemd or the like) so that they may log a transient - * failure and try again. - */ - if (!pgIsNotRunningIsOk) - { - log_error("Failed to get Postgres pid, " - "see above for details"); - } - - /* - * we failed to get Postgres pid from the first line of its pid - * file, so we consider that Postgres is not running, thus not - * ready. - */ - return false; - } - - /* - * When starting up we might read the postmaster.pid file too - * early, when Postgres is still in its "standalone backend" phase. - * Let's give it 250ms before trying again then. - */ - if (pgSetup->pidFile.pid < 0) - { - pg_usleep(250 * 1000); - continue; - } - - /* - * Here, we know that Postgres is running, and we even have its - * PID. Time to try and read the rest of the PID file. This might - * fail when the file isn't complete yet, in which case we're going - * to retry. - */ - if (!read_pg_pidfile(pgSetup, pgIsNotRunningIsOk, maxRetries)) - { - log_warn("Failed to read Postgres \"postmaster.pid\" file"); - return false; - } - - /* avoid an extra wait if that's possible */ - if (pgSetup->pm_status == POSTMASTER_STATUS_READY) - { - break; - } - - log_debug("postmaster status is \"%s\", retrying in %ds.", - pmStatusToString(pgSetup->pm_status), - PG_AUTOCTL_KEEPER_RETRY_TIME_MS); - - pg_usleep(PG_AUTOCTL_KEEPER_RETRY_TIME_MS * 1000); - } - - if (pgSetup->pm_status != POSTMASTER_STATUS_UNKNOWN) - { - log_trace("pg_setup_is_ready: %s", pmStatusToString(pgSetup->pm_status)); - } - - return pgSetup->pm_status == POSTMASTER_STATUS_READY; -} - - -/* - * pg_setup_wait_until_is_ready loops over pg_setup_is_running() and returns - * when Postgres is ready. The loop tries every 100ms up to the given timeout, - * given in seconds. - */ -bool -pg_setup_wait_until_is_ready(PostgresSetup *pgSetup, int timeout, int logLevel) -{ - uint64_t startTime = time(NULL); - int attempts = 0; - - pid_t previousPostgresPid = pgSetup->pidFile.pid; - bool pgIsRunning = false; - bool pgIsReady = false; - - bool missingPgdataIsOk = false; - bool postgresNotRunningIsOk = true; - - log_trace("pg_setup_wait_until_is_ready"); - - for (attempts = 1; !pgIsRunning; attempts++) - { - uint64_t now = time(NULL); - - /* sleep 100 ms in between postmaster.pid probes */ - pg_usleep(100 * 1000); - - pgIsRunning = get_pgpid(pgSetup, postgresNotRunningIsOk) && - pgSetup->pidFile.pid > 0; - - /* let's not be THAT verbose about it */ - if ((attempts - 1) % 10 == 0) - { - log_debug("pg_setup_wait_until_is_ready(): postgres %s, " - "pid %d (was %d), after %ds and %d attempt(s)", - pgIsRunning ? "is running" : "is not running", - pgSetup->pidFile.pid, - previousPostgresPid, - (int) (now - startTime), - attempts); - } - - /* we're done if we reach the timeout */ - if ((now - startTime) >= timeout) - { - break; - } - } - - /* - * Now update our pgSetup from the running database, including versions and - * all we can discover. - */ - if (pgIsRunning && previousPostgresPid != pgSetup->pidFile.pid) - { - /* - * Update our pgSetup view of Postgres once we have made sure it's - * running. - */ - PostgresSetup newPgSetup = { 0 }; - - if (!pg_setup_init(&newPgSetup, - pgSetup, - missingPgdataIsOk, - postgresNotRunningIsOk)) - { - /* errors have already been logged */ - log_error("pg_setup_wait_until_is_ready: pg_setup_init is false"); - return false; - } - - *pgSetup = newPgSetup; - - /* avoid an extra pg_setup_is_ready call if we're all good already */ - pgIsReady = pgSetup->pm_status == POSTMASTER_STATUS_READY; - } - - /* - * Ok so we have a postmaster.pid file with a pid > 0 (not a standalone - * backend, the service has started). Postgres might still be "starting" - * rather than "ready" though, so let's continue our attempts and make sure - * that Postgres is ready. - */ - for (; !pgIsReady; attempts++) - { - uint64_t now = time(NULL); - - pgIsReady = pg_setup_is_ready(pgSetup, postgresNotRunningIsOk); - - /* let's not be THAT verbose about it */ - if ((attempts - 1) % 10 == 0) - { - log_debug("pg_setup_wait_until_is_ready(): pgstatus is %s, " - "pid %d (was %d), after %ds and %d attempt(s)", - pmStatusToString(pgSetup->pm_status), - pgSetup->pidFile.pid, - previousPostgresPid, - (int) (now - startTime), - attempts); - } - - /* we're done if we reach the timeout */ - if ((now - startTime) >= timeout) - { - break; - } - - /* sleep 100 ms in between postmaster.pid probes */ - pg_usleep(100 * 1000); - } - - if (!pgIsReady) - { - /* offer more diagnostic information to the user */ - postgresNotRunningIsOk = false; - pgIsReady = pg_setup_is_ready(pgSetup, postgresNotRunningIsOk); - - log_trace("pg_setup_wait_until_is_ready returns %s [%s]", - pgIsReady ? "true" : "false", - pmStatusToString(pgSetup->pm_status)); - - return pgIsReady; - } - - /* here we know that pgIsReady is true */ - log_level(logLevel, - "Postgres is now serving PGDATA \"%s\" on port %d with pid %d", - pgSetup->pgdata, pgSetup->pgport, pgSetup->pidFile.pid); - return true; -} - - -/* - * pg_setup_wait_until_is_stopped loops over pg_ctl_status() and returns when - * Postgres is stopped. The loop tries every 100ms up to the given timeout, - * given in seconds. - */ -bool -pg_setup_wait_until_is_stopped(PostgresSetup *pgSetup, int timeout, int logLevel) -{ - uint64_t startTime = time(NULL); - int attempts = 0; - int status = -1; - - pid_t previousPostgresPid = pgSetup->pidFile.pid; - - bool missingPgdataIsOk = false; - bool postgresNotRunningIsOk = true; - - for (attempts = 1; status != PG_CTL_STATUS_NOT_RUNNING; attempts++) - { - uint64_t now = time(NULL); - - /* - * If we don't have a postmaster.pid consider that Postgres is not - * running. - */ - if (!get_pgpid(pgSetup, postgresNotRunningIsOk)) - { - return true; - } - - /* we don't log the output for pg_ctl_status here */ - status = pg_ctl_status(pgSetup->pg_ctl, pgSetup->pgdata, false); - - log_trace("keeper_update_postgres_expected_status(): " - "pg_ctl status is %d (we expect %d: not running), " - "after %ds and %d attempt(s)", - status, - PG_CTL_STATUS_NOT_RUNNING, - (int) (now - startTime), - attempts); - - if (status == PG_CTL_STATUS_NOT_RUNNING) - { - return true; - } - - /* we're done if we reach the timeout */ - if ((now - startTime) >= timeout) - { - break; - } - - /* wait for 100 ms and try again */ - pg_usleep(100 * 1000); - } - - /* update settings from running database */ - if (previousPostgresPid != pgSetup->pidFile.pid) - { - /* - * Update our pgSetup view of Postgres once we have made sure it's - * running. - */ - PostgresSetup newPgSetup = { 0 }; - - if (!pg_setup_init(&newPgSetup, - pgSetup, - missingPgdataIsOk, - postgresNotRunningIsOk)) - { - /* errors have already been logged */ - return false; - } - - *pgSetup = newPgSetup; - - log_level(logLevel, - "Postgres is now stopped for PGDATA \"%s\"", - pgSetup->pgdata); - } - - return status == PG_CTL_STATUS_NOT_RUNNING; -} - - -/* - * pg_setup_role returns an enum value representing which role the local - * PostgreSQL instance currently has. We detect primary and secondary when - * Postgres is running, and either recovery or unknown when Postgres is not - * running. - */ -PostgresRole -pg_setup_role(PostgresSetup *pgSetup) -{ - char *pgdata = pgSetup->pgdata; - - if (pg_setup_is_running(pgSetup)) - { - /* - * Here we have either a recovery or a standby node. We don't know for - * sure with just that piece of information. - * - * If we are using Postgres 12+ and there's a standby.signal file in - * PGDATA, that's a strong hint that we can't have in previous version - * short of parsing recovery.conf. - * - * Remember that in versions before Postgres 12 the standby_mode was - * not exposed as a GUC so we can't inquire about that either. We would - * have to parse the recovery.conf file for getting the standby mode. - * - * It's easier to just return POSTGRES_ROLE_RECOVERY in that case, and - * let the caller figure out that this might be POSTGRES_ROLE_STANDBY. - * At the moment the callers don't need that level of detail anyway. - */ - if (pgSetup->is_in_recovery) - { - char recoverySignalPath[MAXPGPATH] = { 0 }; - - join_path_components(recoverySignalPath, pgdata, "standby.signal"); - - if (file_exists(recoverySignalPath)) - { - return POSTGRES_ROLE_STANDBY; - } - else - { - /* We are in recovery, we don't know if we are a standby */ - return POSTGRES_ROLE_RECOVERY; - } - } - - /* - * Here it's running and SELECT pg_is_in_recovery() is false, so we - * know we are talking about a primary server. - */ - else - { - return POSTGRES_ROLE_PRIMARY; - } - } - else - { - /* - * PostgreSQL is not running, we don't know yet... what we know is that - * to be a standby the file $PDGATA/recovery.conf needs to be setup (up - * to version 11 included), or the file $PGDATA/standby.signal needs to - * exists (starting with version 12). A recovery.signal file starting - * in Postgres 12 also indicates that we're not a primary server. - * - * There's no way that a Postgres instance is going to be a recovery or - * standby node without one of those files existing: - */ - char standbyFilesArray[][MAXPGPATH] = { - "recovery.conf", - "recovery.signal", - "standby.signal" - }; - PostgresRole standbyRoleArray[] = { - /* default to recovery, might be a standby */ - POSTGRES_ROLE_RECOVERY, /* recovery.conf */ - POSTGRES_ROLE_RECOVERY, /* recovery.signal */ - POSTGRES_ROLE_STANDBY /* standby.signal */ - }; - int pos = 0, count = 3; - - for (pos = 0; pos < count; pos++) - { - char filePath[MAXPGPATH] = { 0 }; - - join_path_components(filePath, pgdata, standbyFilesArray[pos]); - - if (file_exists(filePath)) - { - return standbyRoleArray[pos]; - } - } - - /* - * Postgres is not running, and there's no file around in PGDATA that - * allows us to have a strong opinion on whether this instance is a - * primary or a standby. It might be either. - */ - return POSTGRES_ROLE_UNKNOWN; - } - - return POSTGRES_ROLE_UNKNOWN; -} - - -/* - * pg_setup_get_username returns pgSetup->username when it exists, otherwise it - * looksup the username in passwd. Lastly it fallsback to the USER environment - * variable. When nothing works it returns DEFAULT_USERNAME PGUSER is only used - * when creating our configuration for the first time. - */ -char * -pg_setup_get_username(PostgresSetup *pgSetup) -{ - char userEnv[NAMEDATALEN] = { 0 }; - - /* use a configured username if provided */ - if (!IS_EMPTY_STRING_BUFFER(pgSetup->username)) - { - return pgSetup->username; - } - - log_trace("username not configured"); - - /* use the passwd file to find the username, same as whoami */ - uid_t uid = geteuid(); - struct passwd *pw = getpwuid(uid); - if (pw) - { - log_trace("username found in passwd: %s", pw->pw_name); - - strlcpy(pgSetup->username, pw->pw_name, sizeof(pgSetup->username)); - return pgSetup->username; - } - - - /* fallback on USER from env if the user cannot be found in passwd */ - if (get_env_copy("USER", userEnv, NAMEDATALEN)) - { - log_trace("username found in USER environment variable: %s", userEnv); - - strlcpy(pgSetup->username, userEnv, sizeof(pgSetup->username)); - return pgSetup->username; - } - - log_trace("username fallback to default: %s", DEFAULT_USERNAME); - strlcpy(pgSetup->username, DEFAULT_USERNAME, sizeof(pgSetup->username)); - - return pgSetup->username; -} - - -/* - * pg_setup_get_auth_method returns pgSetup->authMethod when it exists, - * otherwise it returns DEFAULT_AUTH_METHOD - */ -char * -pg_setup_get_auth_method(PostgresSetup *pgSetup) -{ - if (!IS_EMPTY_STRING_BUFFER(pgSetup->authMethod)) - { - return pgSetup->authMethod; - } - - log_trace("auth method not configured, falling back to default value : %s", - DEFAULT_AUTH_METHOD); - - return DEFAULT_AUTH_METHOD; -} - - -/* - * pg_setup_skip_hba_edits returns true when the user had setup pg_autoctl to - * skip editing HBA entries. - */ -bool -pg_setup_skip_hba_edits(PostgresSetup *pgSetup) -{ - return pgSetup->hbaLevel == HBA_EDIT_SKIP; -} - - -/* - * pg_setup_set_absolute_pgdata uses realpath(3) to make sure that we re using - * the absolute real pathname for PGDATA in our setup, so that services will - * work correctly after keeper/monitor init, even when initializing in a - * relative path and starting the service from elsewhere. This function returns - * true if the pgdata path has been updated in the setup. - */ -bool -pg_setup_set_absolute_pgdata(PostgresSetup *pgSetup) -{ - return normalize_filename(pgSetup->pgdata, pgSetup->pgdata, MAXPGPATH); -} - - -/* - * nodeKindFromString returns a PgInstanceKind from a string. - */ -PgInstanceKind -nodeKindFromString(const char *nodeKind) -{ - PgInstanceKind kindArray[] = { - NODE_KIND_UNKNOWN, - NODE_KIND_UNKNOWN, - NODE_KIND_STANDALONE, - NODE_KIND_CITUS_COORDINATOR, - NODE_KIND_CITUS_WORKER - }; - char *kindList[] = { - "", "unknown", "standalone", "coordinator", "worker", NULL - }; - - for (int listIndex = 0; kindList[listIndex] != NULL; listIndex++) - { - char *candidate = kindList[listIndex]; - - if (strcmp(nodeKind, candidate) == 0) - { - PgInstanceKind pgKind = kindArray[listIndex]; - log_trace("nodeKindFromString: \"%s\" ➜ %d", nodeKind, pgKind); - return pgKind; - } - } - - log_fatal("Failed to parse nodeKind \"%s\"", nodeKind); - - /* never happens, make compiler happy */ - return NODE_KIND_UNKNOWN; -} - - -/* - * nodeKindToString returns a textual representatin of given PgInstanceKind. - * This must be kept in sync with src/monitor/formation_metadata.c function - * FormationKindFromNodeKindString. - */ -char * -nodeKindToString(PgInstanceKind kind) -{ - switch (kind) - { - case NODE_KIND_STANDALONE: - { - return "standalone"; - } - - case NODE_KIND_CITUS_COORDINATOR: - { - return "coordinator"; - } - - case NODE_KIND_CITUS_WORKER: - { - return "worker"; - } - - default: - log_fatal("nodeKindToString: unknown node kind %d", kind); - return NULL; - } - - /* can't happen, keep compiler happy */ - return NULL; -} - - -/* - * pmStatusFromString parses the Postgres postmaster.pid PM_STATUS line into - * our own enum to represent the value. - */ -static PostmasterStatus -pmStatusFromString(const char *postmasterStatus) -{ - if (strcmp(postmasterStatus, PM_STATUS_STARTING) == 0) - { - return POSTMASTER_STATUS_STARTING; - } - else if (strcmp(postmasterStatus, PM_STATUS_STOPPING) == 0) - { - return POSTMASTER_STATUS_STOPPING; - } - else if (strcmp(postmasterStatus, PM_STATUS_READY) == 0) - { - return POSTMASTER_STATUS_READY; - } - else if (strcmp(postmasterStatus, PM_STATUS_STANDBY) == 0) - { - return POSTMASTER_STATUS_STANDBY; - } - - log_warn("Failed to read Postmaster status: \"%s\"", postmasterStatus); - return POSTMASTER_STATUS_UNKNOWN; -} - - -/* - * pmStatusToString returns a textual representation of given Postmaster status - * given as a PmStatus enum. - * - * We're not using the PM_STATUS_READY etc constants here because those are - * blank-padded to always be the same length, and then the warning messages - * including "ready " look buggy in a way. - */ -char * -pmStatusToString(PostmasterStatus pm_status) -{ - switch (pm_status) - { - case POSTMASTER_STATUS_UNKNOWN: - { - return "unknown"; - } - - case POSTMASTER_STATUS_STARTING: - { - return "starting"; - } - - case POSTMASTER_STATUS_STOPPING: - { - return "stopping"; - } - - case POSTMASTER_STATUS_READY: - { - return "ready"; - } - - case POSTMASTER_STATUS_STANDBY: - return "standby"; - } - - /* keep compiler happy */ - return "unknown"; -} - - -/* - * pgsetup_get_pgport returns the port to use either from the PGPORT - * environment variable, or from our default hard-coded value of 5432. - */ -int -pgsetup_get_pgport() -{ - char pgport_env[NAMEDATALEN]; - int pgport = 0; - - if (env_exists("PGPORT") && get_env_copy("PGPORT", pgport_env, NAMEDATALEN)) - { - if (stringToInt(pgport_env, &pgport) && pgport > 0) - { - return pgport; - } - else - { - log_warn("Failed to parse PGPORT value \"%s\", using %d", - pgport_env, POSTGRES_PORT); - return POSTGRES_PORT; - } - } - else - { - /* no PGPORT */ - return POSTGRES_PORT; - } -} - - -/* - * pgsetup_validate_ssl_settings returns true if our SSL settings are following - * one of the three following cases: - * - * - --no-ssl: ssl is not activated and no file has been provided - * - --ssl-self-signed: ssl is activated and no file has been provided - * - --ssl-*-files: ssl is activated and all the files have been provided - * - * Otherwise it logs an error message and return false. - */ -bool -pgsetup_validate_ssl_settings(PostgresSetup *pgSetup) -{ - SSLOptions *ssl = &(pgSetup->ssl); - - log_trace("pgsetup_validate_ssl_settings"); - - /* - * When using the full SSL options, we validate that the files exists where - * given and set the default sslmode to verify-full. - * - * --ssl-ca-file - * --ssl-crl-file - * --server-cert - * --server-key - */ - if (ssl->active && !ssl->createSelfSignedCert) - { - /* - * When passing files in manually for SSL we need at least cert and a - * key - */ - if (IS_EMPTY_STRING_BUFFER(ssl->serverCert) || - IS_EMPTY_STRING_BUFFER(ssl->serverKey)) - { - log_error("Failed to setup SSL with user-provided certificates: " - "options --server-cert and --server-key are required."); - return false; - } - - /* check that the given files exist */ - if (!file_exists(ssl->serverCert)) - { - log_error("--server-cert file does not exist at \"%s\"", - ssl->serverCert); - return false; - } - - if (!file_exists(ssl->serverKey)) - { - log_error("--server-key file does not exist at \"%s\"", - ssl->serverKey); - return false; - } - - if (!IS_EMPTY_STRING_BUFFER(ssl->caFile) && !file_exists(ssl->caFile)) - { - log_error("--ssl-ca-file file does not exist at \"%s\"", - ssl->caFile); - return false; - } - - if (!IS_EMPTY_STRING_BUFFER(ssl->crlFile) && !file_exists(ssl->crlFile)) - { - log_error("--ssl-crl-file file does not exist at \"%s\"", - ssl->crlFile); - return false; - } - - /* install a default value for --ssl-mode, use verify-full */ - if (ssl->sslMode == SSL_MODE_UNKNOWN) - { - ssl->sslMode = SSL_MODE_VERIFY_FULL; - strlcpy(ssl->sslModeStr, - pgsetup_sslmode_to_string(ssl->sslMode), SSL_MODE_STRLEN); - log_info("Using default --ssl-mode \"%s\"", ssl->sslModeStr); - } - - /* check that we have a CA file to use with verif-ca/verify-full */ - if (ssl->sslMode >= SSL_MODE_VERIFY_CA && IS_EMPTY_STRING_BUFFER(ssl->caFile)) - { - log_error("--ssl-ca-file is required when --ssl-mode \"%s\" is used", - ssl->sslModeStr); - return false; - } - - /* - * Normalize the filenames. - * We already log errors so we can simply return the result - */ - return normalize_filename(pgSetup->ssl.caFile, pgSetup->ssl.caFile, - MAXPGPATH) && - normalize_filename(pgSetup->ssl.crlFile, pgSetup->ssl.crlFile, - MAXPGPATH) && - normalize_filename(pgSetup->ssl.serverCert, pgSetup->ssl.serverCert, - MAXPGPATH) && - normalize_filename(pgSetup->ssl.serverKey, pgSetup->ssl.serverKey, - MAXPGPATH); - } - - /* - * When --ssl-self-signed is used, we default to using sslmode=require. - * Setting higher than that are wrong, false sense of security. - */ - if (ssl->createSelfSignedCert) - { - /* in that case we want an sslMode of require at most */ - if (ssl->sslMode > SSL_MODE_REQUIRE) - { - log_error("--ssl-mode \"%s\" is not compatible with self-signed " - "certificates, please provide certificates signed by " - "your trusted CA.", - pgsetup_sslmode_to_string(ssl->sslMode)); - log_info("See https://www.postgresql.org/docs/current/libpq-ssl.html" - " for details"); - return false; - } - - if (ssl->sslMode == SSL_MODE_UNKNOWN) - { - /* install a default value for --ssl-mode */ - ssl->sslMode = SSL_MODE_REQUIRE; - strlcpy(ssl->sslModeStr, - pgsetup_sslmode_to_string(ssl->sslMode), SSL_MODE_STRLEN); - log_info("Using default --ssl-mode \"%s\"", ssl->sslModeStr); - } - - log_info("Using --ssl-self-signed: pg_autoctl will " - "create self-signed certificates, allowing for " - "encrypted network traffic"); - log_warn("Self-signed certificates provide protection against " - "eavesdropping; this setup does NOT protect against " - "Man-In-The-Middle attacks nor Impersonation attacks."); - log_warn("See https://www.postgresql.org/docs/current/libpq-ssl.html " - "for details"); - - return true; - } - - /* --no-ssl is ok */ - if (ssl->active == 0) - { - log_warn("No encryption is used for network traffic! This allows an " - "attacker on the network to read all replication data."); - log_warn("Using --ssl-self-signed instead of --no-ssl is recommend to " - "achieve more security with the same ease of deployment."); - log_warn("See https://www.postgresql.org/docs/current/libpq-ssl.html " - "for details on how to improve"); - - /* Install a default value for --ssl-mode */ - if (ssl->sslMode == SSL_MODE_UNKNOWN) - { - ssl->sslMode = SSL_MODE_PREFER; - strlcpy(ssl->sslModeStr, - pgsetup_sslmode_to_string(ssl->sslMode), SSL_MODE_STRLEN); - log_info("Using default --ssl-mode \"%s\"", ssl->sslModeStr); - } - return true; - } - - return false; -} - - -/* - * pg_setup_sslmode_to_string parses a string representing the sslmode into an - * internal enum value, so that we can easily compare values. - */ -SSLMode -pgsetup_parse_sslmode(const char *sslMode) -{ - SSLMode enumArray[] = { - SSL_MODE_DISABLE, - SSL_MODE_ALLOW, - SSL_MODE_PREFER, - SSL_MODE_REQUIRE, - SSL_MODE_VERIFY_CA, - SSL_MODE_VERIFY_FULL - }; - - char *sslModeArray[] = { - "disable", "allow", "prefer", "require", - "verify-ca", "verify-full", NULL - }; - - int sslModeArrayIndex = 0; - - for (sslModeArrayIndex = 0; - sslModeArray[sslModeArrayIndex] != NULL; - sslModeArrayIndex++) - { - if (strcmp(sslMode, sslModeArray[sslModeArrayIndex]) == 0) - { - return enumArray[sslModeArrayIndex]; - } - } - - return SSL_MODE_UNKNOWN; -} - - -/* - * pgsetup_sslmode_to_string returns the string representation of the enum. - */ -char * -pgsetup_sslmode_to_string(SSLMode sslMode) -{ - switch (sslMode) - { - case SSL_MODE_UNKNOWN: - { - return "unknown"; - } - - case SSL_MODE_DISABLE: - { - return "disable"; - } - - case SSL_MODE_ALLOW: - { - return "allow"; - } - - case SSL_MODE_PREFER: - { - return "prefer"; - } - - case SSL_MODE_REQUIRE: - { - return "require"; - } - - case SSL_MODE_VERIFY_CA: - { - return "verify-ca"; - } - - case SSL_MODE_VERIFY_FULL: - return "verify-full"; - } - - /* This is a huge bug */ - log_error("BUG: some unknown SSL_MODE enum value was encountered"); - return "unknown"; -} - - -/* - * pg_setup_standby_slot_supported returns true when the target Postgres - * instance represented in pgSetup is compatible with using - * pg_replication_slot_advance() on a standby node. - * - * In Postgres 11 and 12, the pg_replication_slot_advance() function has been - * buggy and prevented WAL recycling on standby nodes. - * - * See https://github.com/hapostgres/pg_auto_failover/issues/283 for the problem - * and https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=b48df81 - * for the solution. - * - * We need Postgres 11 starting at 11.9, Postgres 12 starting at 12.4, or - * Postgres 13 or more recent to make use of pg_replication_slot_advance. - */ -bool -pg_setup_standby_slot_supported(PostgresSetup *pgSetup, int logLevel) -{ - int pg_version = 0; - - if (!parse_pg_version_string(pgSetup->pg_version, &pg_version)) - { - /* errors have already been logged */ - return false; - } - - int major = pg_version / 100; - int minor = pg_version % 100; - - /* do we have Postgres 10 (or before, though we don't support that) */ - if (pg_version < 1100) - { - log_trace("pg_setup_standby_slot_supported(%d): false", pg_version); - return false; - } - - /* Postgres 11.0 up to 11.8 included the bug */ - if (pg_version >= 1100 && pg_version < 1109) - { - log_level(logLevel, - "Postgres %d.%d does not support replication slots " - "on a standby node", major, minor); - - return false; - } - - /* Postgres 11.9 and up are good */ - if (pg_version >= 1109 && pg_version < 1200) - { - return true; - } - - /* Postgres 12.0 up to 12.3 included the bug */ - if (pg_version >= 1200 && pg_version < 1204) - { - log_level(logLevel, - "Postgres %d.%d does not support replication slots " - "on a standby node", major, minor); - - return false; - } - - /* Postgres 12.4 and up are good */ - if (pg_version >= 1204 && pg_version < 1300) - { - return true; - } - - /* Starting with Postgres 13, all versions are known to have the bug fix */ - if (pg_version >= 1300) - { - return true; - } - - /* should not happen */ - log_debug("BUG in pg_setup_standby_slot_supported(%d): " - "unknown Postgres version, returning false", - pg_version); - - return false; -} - - -/* - * pgsetup_parse_hba_level parses a string that represents an HBAEditLevel - * value. - */ -HBAEditLevel -pgsetup_parse_hba_level(const char *level) -{ - HBAEditLevel enumArray[] = { - HBA_EDIT_SKIP, - HBA_EDIT_MINIMAL, - HBA_EDIT_LAN - }; - - char *levelArray[] = { "skip", "minimal", "app", NULL }; - - for (int i = 0; levelArray[i] != NULL; i++) - { - if (strcmp(level, levelArray[i]) == 0) - { - return enumArray[i]; - } - } - - return HBA_EDIT_UNKNOWN; -} - - -/* - * pgsetup_hba_level_to_string returns the string representation of an - * hbaLevel enum value. - */ -char * -pgsetup_hba_level_to_string(HBAEditLevel hbaLevel) -{ - switch (hbaLevel) - { - case HBA_EDIT_SKIP: - { - return "skip"; - } - - case HBA_EDIT_MINIMAL: - { - return "minimal"; - } - - case HBA_EDIT_LAN: - { - return "app"; - } - - case HBA_EDIT_UNKNOWN: - return "unknown"; - } - - log_error("BUG: hbaLevel %d is unknown", hbaLevel); - return "unknown"; -} - - -/* - * dbstateToString returns a string from a pgControlFile state enum. - */ -const char * -dbstateToString(DBState state) -{ - switch (state) - { - case DB_STARTUP: - { - return "starting up"; - } - - case DB_SHUTDOWNED: - { - return "shut down"; - } - - case DB_SHUTDOWNED_IN_RECOVERY: - { - return "shut down in recovery"; - } - - case DB_SHUTDOWNING: - { - return "shutting down"; - } - - case DB_IN_CRASH_RECOVERY: - { - return "in crash recovery"; - } - - case DB_IN_ARCHIVE_RECOVERY: - { - return "in archive recovery"; - } - - case DB_IN_PRODUCTION: - return "in production"; - } - return "unrecognized status code"; -} diff --git a/src/bin/pg_autoctl/pgsetup.h b/src/bin/pg_autoctl/pgsetup.h deleted file mode 100644 index f4d4b182d..000000000 --- a/src/bin/pg_autoctl/pgsetup.h +++ /dev/null @@ -1,282 +0,0 @@ -/* - * src/bin/pg_autoctl/pgsetup.h - * Discovers a PostgreSQL setup by calling pg_controldata and reading - * postmaster.pid file, getting clues from the process environment and from - * user given hints (options). - * - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the PostgreSQL License. - * - */ - -#ifndef PGSETUP_H -#define PGSETUP_H - -#include - -#include "postgres_fe.h" - -#include "parson.h" - -/* - * Maximum length of serialized pg_lsn value - * It is taken from postgres file pg_lsn.c. - * It defines MAXPG_LSNLEN to be 17 and - * allocates a buffer 1 byte larger. We - * went for 18 to make buffer allocation simpler. - */ -#define PG_LSN_MAXLENGTH 18 - -/* - * System status indicator. From postgres:src/include/catalog/pg_control.h - */ -typedef enum DBState -{ - DB_STARTUP = 0, - DB_SHUTDOWNED, - DB_SHUTDOWNED_IN_RECOVERY, - DB_SHUTDOWNING, - DB_IN_CRASH_RECOVERY, - DB_IN_ARCHIVE_RECOVERY, - DB_IN_PRODUCTION -} DBState; - -/* - * To be able to check if a minor upgrade should be scheduled, and to check for - * system WAL compatiblity, we use some parts of the pg_controldata output. - * - * See postgresql/src/include/catalog/pg_control.h for definitions of the - * following fields of the ControlFileData struct. - */ -typedef struct pg_control_data -{ - uint64_t system_identifier; - uint32_t pg_control_version; /* PG_CONTROL_VERSION */ - uint32_t catalog_version_no; /* see catversion.h */ - DBState state; /* see enum above */ - char latestCheckpointLSN[PG_LSN_MAXLENGTH]; - uint32_t timeline_id; -} PostgresControlData; - -/* - * We don't need the full information set form the pidfile, it onyl allows us - * to guess/retrieve the PostgreSQL port number from the PGDATA without having - * to ask the user to provide the information. - */ -typedef struct pg_pidfile -{ - pid_t pid; - unsigned short port; -} PostgresPIDFile; - -/* - * From pidfile.h we also extract the Postmaster status, one of the following - * values: - */ -typedef enum -{ - POSTMASTER_STATUS_UNKNOWN = 0, - POSTMASTER_STATUS_STARTING, - POSTMASTER_STATUS_STOPPING, - POSTMASTER_STATUS_READY, - POSTMASTER_STATUS_STANDBY -} PostmasterStatus; - -/* - * When discovering Postgres we try to determine if the local $PGDATA directory - * belongs to a primary or a secondary server. If the server is running, it's - * easy: connect and ask with the pg_is_in_recovery() SQL function. If the - * server is not running, we might be lucky and find a standby setup file and - * then we know it's not a primary. - * - * Otherwise we just don't know. - */ -typedef enum PostgresRole -{ - POSTGRES_ROLE_UNKNOWN, - POSTGRES_ROLE_PRIMARY, - POSTGRES_ROLE_RECOVERY, /* Either PITR or Hot Standby */ - POSTGRES_ROLE_STANDBY /* We know it's an Hot Standby */ -} PostgresRole; - - -/* - * pg_auto_failover knows how to manage three kinds of PostgreSQL servers: - * - * - Standalone PostgreSQL instances - * - Citus Coordinator PostgreSQL instances - * - Citus Worker PostgreSQL instances - * - * Each of them may then take on the role of a primary or a standby depending - * on circumstances. Citus coordinator and worker instances need to load the - * citus extension in shared_preload_libraries, which the keeper ensures. - * - * At failover time, when dealing with a Citus worker instance, the keeper - * fetches its coordinator hostname and port from the monitor and blocks writes - * using the citus master_update_node() function call in a prepared - * transaction. - * - * We use the PgInstanceKind in the FSM as a guard to choose the transition - * function depending on the kind of node we are dealing with. We match the - * node kind using bitwise & operator, so we need to use powers of two here. - * Also we represent "any kind" with the 0xff value. Any all-one value (when - * seen in binary) larger than the max enum value will do really. - */ - -typedef enum PgInstanceKind -{ - NODE_KIND_UNKNOWN = 0, - NODE_KIND_STANDALONE = 1, - NODE_KIND_CITUS_COORDINATOR = 2, - NODE_KIND_CITUS_WORKER = 4, - - NODE_KIND_ANY = 0xff -} PgInstanceKind; - - -#define NODE_KIND_CITUS_ANY \ - (NODE_KIND_CITUS_COORDINATOR | NODE_KIND_CITUS_WORKER) - - -#define IS_CITUS_INSTANCE_KIND(x) \ - (x == NODE_KIND_CITUS_COORDINATOR \ - || x == NODE_KIND_CITUS_WORKER) - -#define pgKind_matches(x, y) ((x & y) != 0) - -#define PG_VERSION_STRING_MAX 12 - -/* - * Monitor keeps a replication settings for each node. - */ -typedef struct NodeReplicationSettings -{ - char name[_POSIX_HOST_NAME_MAX]; - int candidatePriority; /* promotion candidate priority */ - bool replicationQuorum; /* true if participates in write quorum */ -} NodeReplicationSettings; - - -/* - * How much should we edit the Postgres HBA file? - * - * The default value is HBA_EDIT_MINIMAL and pg_autoctl then add entries for - * the monitor to be able to connect to the local node, and an entry for the - * other nodes to be able to connect with streaming replication privileges. - */ -typedef enum -{ - HBA_EDIT_UNKNOWN = 0, - HBA_EDIT_SKIP, - HBA_EDIT_MINIMAL, - HBA_EDIT_LAN, -} HBAEditLevel; - -/* - * pg_auto_failover also support SSL settings. - */ -typedef enum -{ - SSL_MODE_UNKNOWN = 0, - SSL_MODE_DISABLE, - SSL_MODE_ALLOW, - SSL_MODE_PREFER, - SSL_MODE_REQUIRE, - SSL_MODE_VERIFY_CA, - SSL_MODE_VERIFY_FULL -} SSLMode; - -#define SSL_MODE_STRLEN 12 /* longuest is "verify-full" at 11 chars */ - -typedef struct SSLOptions -{ - int active; /* INI support has int, does not have bool */ - bool createSelfSignedCert; - SSLMode sslMode; - char sslModeStr[SSL_MODE_STRLEN]; - char caFile[MAXPGPATH]; - char crlFile[MAXPGPATH]; - char serverCert[MAXPGPATH]; - char serverKey[MAXPGPATH]; -} SSLOptions; - -/* - * In the PostgresSetup structure, we use pghost either as socket directory - * name or as a hostname. We could use MAXPGPATH rather than - * _POSIX_HOST_NAME_MAX chars in that name, but then again the hostname is - * part of a connection string that must be held in MAXCONNINFO. - * - * If you want to change pghost[_POSIX_HOST_NAME_MAX], keep that in mind! - */ -typedef struct pg_setup -{ - char pgdata[MAXPGPATH]; /* PGDATA */ - char pg_ctl[MAXPGPATH]; /* absolute path to pg_ctl */ - char pg_version[PG_VERSION_STRING_MAX]; /* pg_ctl --version */ - char username[NAMEDATALEN]; /* username, defaults to USER */ - char dbname[NAMEDATALEN]; /* dbname, defaults to PGDATABASE */ - char pghost[_POSIX_HOST_NAME_MAX]; /* local PGHOST to connect to */ - int pgport; /* PGPORT */ - char listen_addresses[MAXPGPATH]; /* listen_addresses */ - int proxyport; /* Proxy port */ - char authMethod[NAMEDATALEN]; /* auth method, defaults to trust */ - char hbaLevelStr[NAMEDATALEN]; /* user choice of HBA editing */ - HBAEditLevel hbaLevel; /* user choice of HBA editing */ - PostmasterStatus pm_status; /* Postmaster status */ - bool is_in_recovery; /* select pg_is_in_recovery() */ - PostgresControlData control; /* pg_controldata pgdata */ - PostgresPIDFile pidFile; /* postmaster.pid information */ - PgInstanceKind pgKind; /* standalone/coordinator/worker */ - NodeReplicationSettings settings; /* node replication settings */ - SSLOptions ssl; /* ssl options */ - char citusClusterName[NAMEDATALEN]; /* citus.cluster_name */ -} PostgresSetup; - -#define IS_EMPTY_STRING_BUFFER(strbuf) (strbuf[0] == '\0') - -bool pg_setup_init(PostgresSetup *pgSetup, - PostgresSetup *options, - bool missing_pgdata_is_ok, - bool pg_is_not_running_is_ok); - -bool read_pg_pidfile(PostgresSetup *pgSetup, - bool pgIsNotRunningIsOk, - int maxRetries); - -void fprintf_pg_setup(FILE *stream, PostgresSetup *pgSetup); -bool pg_setup_as_json(PostgresSetup *pgSetup, JSON_Value *js); - -bool pg_setup_get_local_connection_string(PostgresSetup *pgSetup, - char *connectionString); -bool pg_setup_pgdata_exists(PostgresSetup *pgSetup); -bool pg_setup_is_running(PostgresSetup *pgSetup); -PostgresRole pg_setup_role(PostgresSetup *pgSetup); -bool pg_setup_is_ready(PostgresSetup *pgSetup, bool pg_is_not_running_is_ok); -bool pg_setup_wait_until_is_ready(PostgresSetup *pgSetup, - int timeout, int logLevel); -bool pg_setup_wait_until_is_stopped(PostgresSetup *pgSetup, - int timeout, int logLevel); -char * pmStatusToString(PostmasterStatus pm_status); - -char * pg_setup_get_username(PostgresSetup *pgSetup); - -char * pg_setup_get_auth_method(PostgresSetup *pgSetup); -bool pg_setup_skip_hba_edits(PostgresSetup *pgSetup); - -bool pg_setup_set_absolute_pgdata(PostgresSetup *pgSetup); - -PgInstanceKind nodeKindFromString(const char *nodeKind); -char * nodeKindToString(PgInstanceKind kind); -int pgsetup_get_pgport(void); - -bool pgsetup_validate_ssl_settings(PostgresSetup *pgSetup); -SSLMode pgsetup_parse_sslmode(const char *sslMode); -char * pgsetup_sslmode_to_string(SSLMode sslMode); - -bool pg_setup_standby_slot_supported(PostgresSetup *pgSetup, int logLevel); - -HBAEditLevel pgsetup_parse_hba_level(const char *level); -char * pgsetup_hba_level_to_string(HBAEditLevel hbaLevel); -const char * dbstateToString(DBState state); - -#endif /* PGSETUP_H */ diff --git a/src/bin/pg_autoctl/pgsql.c b/src/bin/pg_autoctl/pgsql.c deleted file mode 100644 index 450c7e1a5..000000000 --- a/src/bin/pg_autoctl/pgsql.c +++ /dev/null @@ -1,3390 +0,0 @@ -/* - * src/bin/pg_autoctl/pgsql.c - * API for sending SQL commands to a PostgreSQL server - * - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the PostgreSQL License. - * - */ -#include -#include -#include - -#include "postgres.h" -#include "postgres_fe.h" -#include "libpq-fe.h" -#include "pqexpbuffer.h" -#include "portability/instr_time.h" - -#if PG_MAJORVERSION_NUM >= 15 -#include "common/pg_prng.h" -#endif - -#include "cli_root.h" -#include "defaults.h" -#include "log.h" -#include "parsing.h" -#include "pgsql.h" -#include "signals.h" -#include "string_utils.h" - - -#define STR_ERRCODE_DUPLICATE_OBJECT "42710" -#define STR_ERRCODE_DUPLICATE_DATABASE "42P04" - -#define STR_ERRCODE_INVALID_OBJECT_DEFINITION "42P17" -#define STR_ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE "55000" -#define STR_ERRCODE_OBJECT_IN_USE "55006" -#define STR_ERRCODE_UNDEFINED_OBJECT "42704" - -static char * ConnectionTypeToString(ConnectionType connectionType); -static void log_connection_error(PGconn *connection, int logLevel); -static void pgAutoCtlDefaultNoticeProcessor(void *arg, const char *message); -static void pgAutoCtlDebugNoticeProcessor(void *arg, const char *message); -static PGconn * pgsql_open_connection(PGSQL *pgsql); -static bool pgsql_retry_open_connection(PGSQL *pgsql); -static bool is_response_ok(PGresult *result); -static bool clear_results(PGSQL *pgsql); -static void pgsql_handle_notifications(PGSQL *pgsql); -static bool pgsql_alter_system_set(PGSQL *pgsql, GUC setting); -static bool pgsql_get_current_setting(PGSQL *pgsql, char *settingName, - char **currentValue); -static void parsePgMetadata(void *ctx, PGresult *result); -static void parsePgReachedTargetLSN(void *ctx, PGresult *result); -static void parseReplicationSlotMaintain(void *ctx, PGresult *result); -static void parsePgReachedTargetLSN(void *ctx, PGresult *result); -static void parseIdentifySystemResult(void *ctx, PGresult *result); -static void parseTimelineHistoryResult(void *ctx, PGresult *result); - - -/* - * parseSingleValueResult is a ParsePostgresResultCB callback that reads the - * first column of the first row of the resultset only, and parses the answer - * into the expected C value, one of type QueryResultType. - */ -void -parseSingleValueResult(void *ctx, PGresult *result) -{ - SingleValueResultContext *context = (SingleValueResultContext *) ctx; - - context->ntuples = PQntuples(result); - - if (context->ntuples == 1) - { - char *value = PQgetvalue(result, 0, 0); - - /* this function is never used when we expect NULL values */ - if (PQgetisnull(result, 0, 0)) - { - context->parsedOk = false; - return; - } - - switch (context->resultType) - { - case PGSQL_RESULT_BOOL: - { - context->boolVal = strcmp(value, "t") == 0; - context->parsedOk = true; - break; - } - - case PGSQL_RESULT_INT: - { - if (!stringToInt(value, &context->intVal)) - { - context->parsedOk = false; - log_error("Failed to parse int result \"%s\"", value); - } - context->parsedOk = true; - break; - } - - case PGSQL_RESULT_BIGINT: - { - if (!stringToUInt64(value, &context->bigint)) - { - context->parsedOk = false; - log_error("Failed to parse uint64_t result \"%s\"", value); - } - context->parsedOk = true; - break; - } - - case PGSQL_RESULT_STRING: - { - context->strVal = strdup(value); - if (context->strVal == NULL) - { - context->parsedOk = false; - log_error(ALLOCATION_FAILED_ERROR); - } - context->parsedOk = true; - break; - } - } - } -} - - -/* - * fetchedRows is a pgsql_execute_with_params callback function that sets a - * SingleValueResultContext->intVal to PQntuples(result), that is how many rows - * are fetched by the query. - */ -void -fetchedRows(void *ctx, PGresult *result) -{ - SingleValueResultContext *context = (SingleValueResultContext *) ctx; - - context->parsedOk = true; - context->intVal = PQntuples(result); -} - - -/* - * pgsql_init initializes a PGSQL struct to connect to the given database - * URL or connection string. - */ -bool -pgsql_init(PGSQL *pgsql, char *url, ConnectionType connectionType) -{ - pgsql->connectionType = connectionType; - pgsql->connection = NULL; - - /* set our default retry policy for interactive commands */ - (void) pgsql_set_interactive_retry_policy(&(pgsql->retryPolicy)); - - if (validate_connection_string(url)) - { - /* size of url has already been validated. */ - strlcpy(pgsql->connectionString, url, MAXCONNINFO); - } - else - { - return false; - } - return true; -} - - -/* - * pgsql_set_retry_policy sets the retry policy to the given maxT (maximum - * total time spent retrying), maxR (maximum number of retries, zero when not - * retrying at all, -1 for unbounded number of retries), and maxSleepTime to - * cap our exponential backoff with decorrelated jitter computation. - */ -void -pgsql_set_retry_policy(ConnectionRetryPolicy *retryPolicy, - int maxT, - int maxR, - int maxSleepTime, - int baseSleepTime) -{ - retryPolicy->maxT = maxT; - retryPolicy->maxR = maxR; - retryPolicy->maxSleepTime = maxSleepTime; - retryPolicy->baseSleepTime = baseSleepTime; - - /* initialize a seed for our random number generator */ -#if PG_MAJORVERSION_NUM < 15 - pg_srand48(time(0)); -#else - pg_prng_seed(&(retryPolicy->prng_state), (uint64) (getpid() ^ time(NULL))); -#endif -} - - -/* - * pgsql_set_default_retry_policy sets the default retry policy: no retry. We - * use the other default parameters but with a maxR of zero they don't get - * used. - * - * This is the retry policy that prevails in the main keeper loop. - */ -void -pgsql_set_main_loop_retry_policy(ConnectionRetryPolicy *retryPolicy) -{ - (void) pgsql_set_retry_policy(retryPolicy, - POSTGRES_PING_RETRY_TIMEOUT, - 0, /* do not retry by default */ - POSTGRES_PING_RETRY_CAP_SLEEP_TIME, - POSTGRES_PING_RETRY_BASE_SLEEP_TIME); -} - - -/* - * pgsql_set_init_retry_policy sets the retry policy to 15 mins of total - * retrying time, unbounded number of attempts, and up to 2 seconds of sleep - * time in between attempts. - * - * This is the policy that we use in keeper_register_and_init. When using - * automated provisioning tools and frameworks, it might be that every node is - * provisionned concurrently and we might try to connect to the monitor before - * it's ready. In that case we want to retry for a long time. - */ -void -pgsql_set_init_retry_policy(ConnectionRetryPolicy *retryPolicy) -{ - (void) pgsql_set_retry_policy(retryPolicy, - POSTGRES_PING_RETRY_TIMEOUT, - -1, /* unbounded number of attempts */ - POSTGRES_PING_RETRY_CAP_SLEEP_TIME, - POSTGRES_PING_RETRY_BASE_SLEEP_TIME); -} - - -/* - * pgsql_set_interactive_retry_policy sets the retry policy to 2 seconds of - * total retrying time (or PGCONNECT_TIMEOUT when that's set), unbounded number - * of attempts, and up to 2 seconds of sleep time in between attempts. - */ -void -pgsql_set_interactive_retry_policy(ConnectionRetryPolicy *retryPolicy) -{ - (void) pgsql_set_retry_policy(retryPolicy, - pgconnect_timeout, - -1, /* unbounded number of attempts */ - POSTGRES_PING_RETRY_CAP_SLEEP_TIME, - POSTGRES_PING_RETRY_BASE_SLEEP_TIME); -} - - -/* - * pgsql_set_monitor_interactive_retry_policy sets the retry policy to 15 mins - * of total retrying time, unbounded number of attemps, and up to 5 seconds of - * sleep time in between attemps, starting at 1 second for the first retry. - * - * We use this policy in interactive commands when connecting to the monitor, - * such as when doing pg_autoctl enable|disable maintenance. - */ -void -pgsql_set_monitor_interactive_retry_policy(ConnectionRetryPolicy *retryPolicy) -{ - int cap = 5 * 1000; /* sleep up to 5s between attempts */ - int sleepTime = 1 * 1000; /* first retry happens after 1 second */ - - (void) pgsql_set_retry_policy(retryPolicy, - POSTGRES_PING_RETRY_TIMEOUT, - -1, /* unbounded number of attempts */ - cap, - sleepTime); -} - - -#define min(a, b) (a < b ? a : b) - -/* - * http://c-faq.com/lib/randrange.html - */ -#define random_between(R, M, N) ((M) + R / (RAND_MAX / ((N) -(M) +1) + 1)) - -/* - * pick_random_sleep_time picks a random sleep time between the given policy - * base sleep time and 3 times the previous sleep time. See below in - * pgsql_compute_connection_retry_sleep_time for a deep dive into why we are - * interested in this computation. - */ -static int -pick_random_sleep_time(ConnectionRetryPolicy *retryPolicy) -{ -#if PG_MAJORVERSION_NUM < 15 - long random = pg_lrand48(); -#else - uint32_t random = pg_prng_uint32(&(retryPolicy->prng_state)); -#endif - - return random_between(random, - retryPolicy->baseSleepTime, - retryPolicy->sleepTime * 3); -} - - -/* - * pgsql_compute_connection_retry_sleep_time returns how much time to sleep - * this time, in milliseconds. - */ -int -pgsql_compute_connection_retry_sleep_time(ConnectionRetryPolicy *retryPolicy) -{ - /* - * https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/ - * - * Adding jitter is a small change to the sleep function: - * - * sleep = random_between(0, min(cap, base * 2^attempt)) - * - * There are a few ways to implement these timed backoff loops. Let’s call - * the algorithm above “Full Jitter”, and consider two alternatives. The - * first alternative is “Equal Jitter”, where we always keep some of the - * backoff and jitter by a smaller amount: - * - * temp = min(cap, base * 2^attempt) - * sleep = temp/2 + random_between(0, temp/2) - * - * The intuition behind this one is that it prevents very short sleeps, - * always keeping some of the slow down from the backoff. - * - * A second alternative is “Decorrelated Jitter”, which is similar to “Full - * Jitter”, but we also increase the maximum jitter based on the last - * random value. - * - * sleep = min(cap, random_between(base, sleep*3)) - * - * Which approach do you think is best? - * - * The no-jitter exponential backoff approach is the clear loser. [...] - * - * Of the jittered approaches, “Equal Jitter” is the loser. It does - * slightly more work than “Full Jitter”, and takes much longer. The - * decision between “Decorrelated Jitter” and “Full Jitter” is less clear. - * The “Full Jitter” approach uses less work, but slightly more time. Both - * approaches, though, present a substantial decrease in client work and - * server load. - * - * Here we implement "Decorrelated Jitter", which is better in terms of - * time spent, something we care to optimize for even when it means more - * work on the monitor side. - */ - int sleepTime = pick_random_sleep_time(retryPolicy); - - retryPolicy->sleepTime = min(retryPolicy->maxSleepTime, sleepTime); - - ++(retryPolicy->attempts); - - return retryPolicy->sleepTime; -} - - -/* - * pgsql_retry_policy_expired returns true when we should stop retrying, either - * per the policy (maxR / maxT) or because we received a signal that we have to - * obey. - */ -bool -pgsql_retry_policy_expired(ConnectionRetryPolicy *retryPolicy) -{ - instr_time duration; - - /* Any signal is reason enough to break out from this retry loop. */ - if (asked_to_quit || asked_to_stop || asked_to_stop_fast || asked_to_reload) - { - return true; - } - - /* set the first retry time when it's not been set previously */ - if (INSTR_TIME_IS_ZERO(retryPolicy->startTime)) - { - INSTR_TIME_SET_CURRENT(retryPolicy->startTime); - } - - INSTR_TIME_SET_CURRENT(duration); - INSTR_TIME_SUBTRACT(duration, retryPolicy->startTime); - - /* - * We stop retrying as soon as we have spent all of our time budget or all - * of our attempts count budget, whichever comes first. - * - * maxR = 0 (zero) means no retry at all, checked before the loop - * maxR < 0 (zero) means unlimited number of retries - */ - if ((INSTR_TIME_GET_MILLISEC(duration) >= (retryPolicy->maxT * 1000)) || - (retryPolicy->maxR > 0 && - retryPolicy->attempts >= retryPolicy->maxR)) - { - return true; - } - - return false; -} - - -/* - * connectionTypeToString transforms a connectionType in a string to be used in - * a user facing message. - */ -static char * -ConnectionTypeToString(ConnectionType connectionType) -{ - switch (connectionType) - { - case PGSQL_CONN_LOCAL: - { - return "local"; - } - - case PGSQL_CONN_MONITOR: - { - return "monitor"; - } - - case PGSQL_CONN_COORDINATOR: - { - return "coordinator"; - } - - case PGSQL_CONN_UPSTREAM: - { - return "upstream"; - } - - default: - { - return "unknown connection type"; - } - } -} - - -/* - * Finish a PGSQL client connection. - */ -void -pgsql_finish(PGSQL *pgsql) -{ - if (pgsql->connection != NULL) - { - char scrubbedConnectionString[MAXCONNINFO] = { 0 }; - - if (!parse_and_scrub_connection_string(pgsql->connectionString, - scrubbedConnectionString)) - { - log_debug("Failed to scrub password from connection string"); - - strlcpy(scrubbedConnectionString, - pgsql->connectionString, - sizeof(scrubbedConnectionString)); - } - - log_debug("Disconnecting from [%s] \"%s\"", - ConnectionTypeToString(pgsql->connectionType), - scrubbedConnectionString); - PQfinish(pgsql->connection); - pgsql->connection = NULL; - - /* - * When we fail to connect, on the way out we call pgsql_finish to - * reset the connection to NULL. We still want the callers to be able - * to inquire about our connection status, so refrain to reset the - * status. - */ - } - - pgsql->connectionStatementType = PGSQL_CONNECTION_SINGLE_STATEMENT; -} - - -/* - * log_connection_error logs the PQerrorMessage from the given connection. - */ -static void -log_connection_error(PGconn *connection, int logLevel) -{ - char *message = connection != NULL ? PQerrorMessage(connection) : NULL; - char *errorLines[BUFSIZE] = { 0 }; - int lineCount = splitLines(message, errorLines, BUFSIZE); - int lineNumber = 0; - - /* PQerrorMessage is then "connection pointer is NULL", not helpful */ - if (connection == NULL) - { - return; - } - - for (lineNumber = 0; lineNumber < lineCount; lineNumber++) - { - char *line = errorLines[lineNumber]; - - if (lineNumber == 0) - { - log_level(logLevel, "Connection to database failed: %s", line); - } - else - { - log_level(logLevel, "%s", line); - } - } -} - - -/* - * pgsql_open_connection opens a PostgreSQL connection, given a PGSQL client - * instance. If a connection is already open in the client (it's not NULL), - * then this errors, unless we are inside a transaction opened by pgsql_begin. - */ -static PGconn * -pgsql_open_connection(PGSQL *pgsql) -{ - /* we might be connected already */ - if (pgsql->connection != NULL) - { - if (pgsql->connectionStatementType != PGSQL_CONNECTION_MULTI_STATEMENT) - { - log_error("BUG: requested to open an already open connection in " - "non PGSQL_CONNECTION_MULTI_STATEMENT mode"); - pgsql_finish(pgsql); - return NULL; - } - return pgsql->connection; - } - - char scrubbedConnectionString[MAXCONNINFO] = { 0 }; - - (void) parse_and_scrub_connection_string(pgsql->connectionString, - scrubbedConnectionString); - - log_debug("Connecting to [%s] \"%s\"", - ConnectionTypeToString(pgsql->connectionType), - scrubbedConnectionString); - - /* we implement our own retry strategy */ - setenv("PGCONNECT_TIMEOUT", POSTGRES_CONNECT_TIMEOUT, 1); - - /* register our starting time */ - INSTR_TIME_SET_CURRENT(pgsql->retryPolicy.startTime); - INSTR_TIME_SET_ZERO(pgsql->retryPolicy.connectTime); - - /* Make a connection to the database */ - pgsql->connection = PQconnectdb(pgsql->connectionString); - - /* Check to see that the backend connection was successfully made */ - if (PQstatus(pgsql->connection) != CONNECTION_OK) - { - /* - * Implement the retry policy: - * - * First observe the maxR property: maximum retries allowed. When set - * to zero, we don't retry at all. - */ - if (pgsql->retryPolicy.maxR == 0) - { - INSTR_TIME_SET_CURRENT(pgsql->retryPolicy.connectTime); - - (void) log_connection_error(pgsql->connection, LOG_ERROR); - - log_error("Failed to connect to %s database at \"%s\", " - "see above for details", - ConnectionTypeToString(pgsql->connectionType), - pgsql->connectionString); - - pgsql->status = PG_CONNECTION_BAD; - - pgsql_finish(pgsql); - return NULL; - } - - /* - * If we reach this part of the code, the connectionType is not LOCAL - * and the retryPolicy has a non-zero maximum retry count. Let's retry! - */ - if (!pgsql_retry_open_connection(pgsql)) - { - /* errors have already been logged */ - return NULL; - } - } - - INSTR_TIME_SET_CURRENT(pgsql->retryPolicy.connectTime); - pgsql->status = PG_CONNECTION_OK; - - /* set the libpq notice receiver to integrate notifications as warnings. */ - PQsetNoticeProcessor(pgsql->connection, - &pgAutoCtlDefaultNoticeProcessor, - NULL); - - return pgsql->connection; -} - - -/* - * Refrain from warning too often. The user certainly wants to know that we are - * still trying to connect, though warning several times a second is not going - * to help anyone. A good trade-off seems to be a warning every 30s. - */ -#define SHOULD_WARN_AGAIN(duration) \ - (INSTR_TIME_GET_MILLISEC(duration) > 30000) - -/* - * pgsql_retry_open_connection loops over a PQping call until the remote server - * is ready to accept connections, and then connects to it and returns true - * when it could connect, false otherwise. - */ -static bool -pgsql_retry_open_connection(PGSQL *pgsql) -{ - bool connectionOk = false; - - PGPing lastWarningMessage = PQPING_OK; - instr_time lastWarningTime; - - INSTR_TIME_SET_ZERO(lastWarningTime); - - char scrubbedConnectionString[MAXCONNINFO] = { 0 }; - - (void) parse_and_scrub_connection_string(pgsql->connectionString, - scrubbedConnectionString); - - log_warn("Failed to connect to \"%s\", retrying until " - "the server is ready", scrubbedConnectionString); - - /* should not happen */ - if (pgsql->retryPolicy.maxR == 0) - { - return false; - } - - /* reset our internal counter before entering the retry loop */ - pgsql->retryPolicy.attempts = 1; - - while (!connectionOk) - { - if (pgsql_retry_policy_expired(&(pgsql->retryPolicy))) - { - instr_time duration; - - INSTR_TIME_SET_CURRENT(duration); - INSTR_TIME_SUBTRACT(duration, pgsql->retryPolicy.startTime); - - (void) log_connection_error(pgsql->connection, LOG_ERROR); - pgsql->status = PG_CONNECTION_BAD; - pgsql_finish(pgsql); - - log_error("Failed to connect to \"%s\" " - "after %d attempts in %d ms, " - "pg_autoctl stops retrying now", - scrubbedConnectionString, - pgsql->retryPolicy.attempts, - (int) INSTR_TIME_GET_MILLISEC(duration)); - - return false; - } - - /* - * Now compute how much time to wait for this round, and increment how - * many times we tried to connect already. - */ - int sleep = - pgsql_compute_connection_retry_sleep_time(&(pgsql->retryPolicy)); - - /* we have milliseconds, pg_usleep() wants microseconds */ - (void) pg_usleep(sleep * 1000); - - log_debug("PQping(%s): slept %d ms on attempt %d", - scrubbedConnectionString, - pgsql->retryPolicy.sleepTime, - pgsql->retryPolicy.attempts); - - switch (PQping(pgsql->connectionString)) - { - /* - * https://www.postgresql.org/docs/current/libpq-connect.html - * - * The server is running and appears to be accepting connections. - */ - case PQPING_OK: - { - log_debug("PQping OK after %d attempts", - pgsql->retryPolicy.attempts); - - /* - * Ping is now ok, and connection is still NULL because the - * first attempt to connect failed. Now is a good time to - * establish the connection. - * - * PQping does not check authentication, so we might still fail - * to connect to the server. - */ - pgsql->connection = PQconnectdb(pgsql->connectionString); - - if (PQstatus(pgsql->connection) == CONNECTION_OK) - { - instr_time duration; - - INSTR_TIME_SET_CURRENT(duration); - - connectionOk = true; - pgsql->status = PG_CONNECTION_OK; - pgsql->retryPolicy.connectTime = duration; - - INSTR_TIME_SUBTRACT(duration, pgsql->retryPolicy.startTime); - - log_info("Successfully connected to \"%s\" " - "after %d attempts in %d ms.", - scrubbedConnectionString, - pgsql->retryPolicy.attempts, - (int) INSTR_TIME_GET_MILLISEC(duration)); - } - else - { - instr_time durationSinceLastWarning; - - INSTR_TIME_SET_CURRENT(durationSinceLastWarning); - INSTR_TIME_SUBTRACT(durationSinceLastWarning, lastWarningTime); - - if (lastWarningMessage != PQPING_OK || - SHOULD_WARN_AGAIN(durationSinceLastWarning)) - { - lastWarningMessage = PQPING_OK; - INSTR_TIME_SET_CURRENT(lastWarningTime); - - /* - * Only show details when that's the last attempt, - * otherwise accept that this may happen as a transient - * state. - */ - (void) log_connection_error(pgsql->connection, LOG_DEBUG); - - log_debug("Failed to connect after successful ping"); - } - } - break; - } - - /* - * https://www.postgresql.org/docs/current/libpq-connect.html - * - * The server is running but is in a state that disallows - * connections (startup, shutdown, or crash recovery). - */ - case PQPING_REJECT: - { - instr_time durationSinceLastWarning; - - INSTR_TIME_SET_CURRENT(durationSinceLastWarning); - INSTR_TIME_SUBTRACT(durationSinceLastWarning, lastWarningTime); - - if (lastWarningMessage != PQPING_REJECT || - SHOULD_WARN_AGAIN(durationSinceLastWarning)) - { - lastWarningMessage = PQPING_REJECT; - INSTR_TIME_SET_CURRENT(lastWarningTime); - - log_warn( - "The server at \"%s\" is running but is in a state " - "that disallows connections (startup, shutdown, or " - "crash recovery).", - scrubbedConnectionString); - } - - break; - } - - /* - * https://www.postgresql.org/docs/current/libpq-connect.html - * - * The server could not be contacted. This might indicate that the - * server is not running, or that there is something wrong with the - * given connection parameters (for example, wrong port number), or - * that there is a network connectivity problem (for example, a - * firewall blocking the connection request). - */ - case PQPING_NO_RESPONSE: - { - instr_time durationSinceStart, durationSinceLastWarning; - - INSTR_TIME_SET_CURRENT(durationSinceStart); - INSTR_TIME_SUBTRACT(durationSinceStart, - pgsql->retryPolicy.startTime); - - INSTR_TIME_SET_CURRENT(durationSinceLastWarning); - INSTR_TIME_SUBTRACT(durationSinceLastWarning, lastWarningTime); - - /* no message at all the first 30s: 30000ms */ - if (SHOULD_WARN_AGAIN(durationSinceStart) && - (lastWarningMessage != PQPING_NO_RESPONSE || - SHOULD_WARN_AGAIN(durationSinceLastWarning))) - { - lastWarningMessage = PQPING_NO_RESPONSE; - INSTR_TIME_SET_CURRENT(lastWarningTime); - - log_warn( - "The server at \"%s\" could not be contacted " - "after %d attempts in %d ms (milliseconds). " - "This might indicate that the server is not running, " - "or that there is something wrong with the given " - "connection parameters (for example, wrong port " - "number), or that there is a network connectivity " - "problem (for example, a firewall blocking the " - "connection request).", - scrubbedConnectionString, - pgsql->retryPolicy.attempts, - (int) INSTR_TIME_GET_MILLISEC(durationSinceStart)); - } - - break; - } - - /* - * https://www.postgresql.org/docs/current/libpq-connect.html - * - * No attempt was made to contact the server, because the supplied - * parameters were obviously incorrect or there was some - * client-side problem (for example, out of memory). - */ - case PQPING_NO_ATTEMPT: - { - lastWarningMessage = PQPING_NO_ATTEMPT; - log_debug("Failed to ping server \"%s\" because of " - "client-side problems (no attempt were made)", - scrubbedConnectionString); - break; - } - } - } - - if (!connectionOk && pgsql->connection != NULL) - { - INSTR_TIME_SET_CURRENT(pgsql->retryPolicy.connectTime); - - (void) log_connection_error(pgsql->connection, LOG_ERROR); - pgsql->status = PG_CONNECTION_BAD; - pgsql_finish(pgsql); - - return false; - } - - return true; -} - - -/* - * pgAutoCtlDefaultNoticeProcessor is our default PostgreSQL libpq Notice - * Processing: NOTICE, WARNING, HINT etc are processed as log_warn messages by - * default. - */ -static void -pgAutoCtlDefaultNoticeProcessor(void *arg, const char *message) -{ - char *m = strdup(message); - char *lines[BUFSIZE]; - int lineCount = splitLines(m, lines, BUFSIZE); - int lineNumber = 0; - - for (lineNumber = 0; lineNumber < lineCount; lineNumber++) - { - log_warn("%s", lines[lineNumber]); - } - - free(m); -} - - -/* - * pgAutoCtlDebugNoticeProcessor is our PostgreSQL libpq Notice Processing to - * use when wanting to send NOTICE, WARNING, HINT as log_debug messages. - */ -static void -pgAutoCtlDebugNoticeProcessor(void *arg, const char *message) -{ - char *m = strdup(message); - char *lines[BUFSIZE]; - int lineCount = splitLines(m, lines, BUFSIZE); - int lineNumber = 0; - - for (lineNumber = 0; lineNumber < lineCount; lineNumber++) - { - log_debug("%s", lines[lineNumber]); - } - - free(m); -} - - -/* - * pgsql_begin is responsible for opening a mutli statement connection and - * opening a transaction block by issuing a 'BEGIN' query. - */ -bool -pgsql_begin(PGSQL *pgsql) -{ - /* - * Indicate that we're running a transaction, so that the connection is not - * closed after each query automatically. It also allows us to detect bugs - * easily. We need to do this before executing BEGIN, because otherwise the - * connection is closed after the BEGIN statement automatically. - */ - pgsql->connectionStatementType = PGSQL_CONNECTION_MULTI_STATEMENT; - - if (!pgsql_execute(pgsql, "BEGIN")) - { - /* - * We need to manually call pgsql_finish to clean up here in case of - * this failure, because we have set the statement type to MULTI. - */ - pgsql_finish(pgsql); - return false; - } - - return true; -} - - -/* - * pgsql_rollback is responsible for issuing a 'ROLLBACK' query to an already - * opened transaction, usually via a previous pgsql_begin() command. - * - * It closes the connection but leaves the error contents, if any, for the user - * to examine should it is wished for. - */ -bool -pgsql_rollback(PGSQL *pgsql) -{ - bool result; - - if (pgsql->connectionStatementType != PGSQL_CONNECTION_MULTI_STATEMENT || - pgsql->connection == NULL) - { - log_error("BUG: call to pgsql_rollback without holding an open " - "multi statement connection"); - return false; - } - - result = pgsql_execute(pgsql, "ROLLBACK"); - - /* - * Connection might be be closed during the pgsql_execute(), notably in case - * of error. Be explicit and close it regardless though. - */ - if (pgsql->connection) - { - pgsql_finish(pgsql); - } - - return result; -} - - -/* - * pgsql_commit is responsible for issuing a 'COMMIT' query to an already - * opened transaction, usually via a previous pgsql_begin() command. - * - * It closes the connection but leaves the error contents, if any, for the user - * to examine should it is wished for. - */ -bool -pgsql_commit(PGSQL *pgsql) -{ - bool result; - - if (pgsql->connectionStatementType != PGSQL_CONNECTION_MULTI_STATEMENT || - pgsql->connection == NULL) - { - log_error("BUG: call to pgsql_commit() without holding an open " - "multi statement connection"); - if (pgsql->connection) - { - pgsql_finish(pgsql); - } - return false; - } - - result = pgsql_execute(pgsql, "COMMIT"); - - /* - * Connection might be be closed during the pgsql_execute(), notably in case - * of error. Be explicit and close it regardless though. - */ - if (pgsql->connection) - { - pgsql_finish(pgsql); - } - - return result; -} - - -/* - * pgsql_execute opens a connection, runs a given SQL command, and closes - * the connection again. - * - * We avoid persisting connection across multiple commands to simplify error - * handling. - */ -bool -pgsql_execute(PGSQL *pgsql, const char *sql) -{ - return pgsql_execute_with_params(pgsql, sql, 0, NULL, NULL, NULL, NULL); -} - - -/* - * pgsql_execute_with_params opens a connection, runs a given SQL command, - * and closes the connection again. - * - * We avoid persisting connection across multiple commands to simplify error - * handling. - */ -bool -pgsql_execute_with_params(PGSQL *pgsql, const char *sql, int paramCount, - const Oid *paramTypes, const char **paramValues, - void *context, ParsePostgresResultCB *parseFun) -{ - char debugParameters[BUFSIZE] = { 0 }; - PGresult *result = NULL; - - PGconn *connection = pgsql_open_connection(pgsql); - - if (connection == NULL) - { - return false; - } - - log_debug("%s;", sql); - - if (paramCount > 0) - { - int paramIndex = 0; - int remainingBytes = BUFSIZE; - char *writePointer = (char *) debugParameters; - - for (paramIndex = 0; paramIndex < paramCount; paramIndex++) - { - int bytesWritten = 0; - const char *value = paramValues[paramIndex]; - - if (paramIndex > 0) - { - bytesWritten = sformat(writePointer, remainingBytes, ", "); - remainingBytes -= bytesWritten; - writePointer += bytesWritten; - } - - if (value == NULL) - { - bytesWritten = sformat(writePointer, remainingBytes, "NULL"); - } - else - { - bytesWritten = - sformat(writePointer, remainingBytes, "'%s'", value); - } - remainingBytes -= bytesWritten; - writePointer += bytesWritten; - } - log_debug("%s", debugParameters); - } - - if (paramCount == 0) - { - result = PQexec(connection, sql); - } - else - { - result = PQexecParams(connection, sql, - paramCount, paramTypes, paramValues, - NULL, NULL, 0); - } - - if (!is_response_ok(result)) - { - char *sqlstate = PQresultErrorField(result, PG_DIAG_SQLSTATE); - char *message = PQerrorMessage(connection); - char *errorLines[BUFSIZE]; - int lineCount = splitLines(message, errorLines, BUFSIZE); - int lineNumber = 0; - - char *prefix = - pgsql->connectionType == PGSQL_CONN_MONITOR ? "Monitor" : "Postgres"; - - /* - * PostgreSQL Error message might contain several lines. Log each of - * them as a separate ERROR line here. - */ - for (lineNumber = 0; lineNumber < lineCount; lineNumber++) - { - log_error("%s %s", prefix, errorLines[lineNumber]); - } - - /* - * The monitor uses those error codes in situations we know how to - * handle, so if we have one of those, it's not a client-side error - * with a badly formed SQL query etc. - */ - if (pgsql->connectionType == PGSQL_CONN_MONITOR && - sqlstate != NULL && - !(strcmp(sqlstate, STR_ERRCODE_INVALID_OBJECT_DEFINITION) == 0 || - strcmp(sqlstate, STR_ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE) == 0 || - strcmp(sqlstate, STR_ERRCODE_OBJECT_IN_USE) == 0 || - strcmp(sqlstate, STR_ERRCODE_UNDEFINED_OBJECT) == 0)) - { - log_error("SQL query: %s", sql); - log_error("SQL params: %s", debugParameters); - } - else - { - log_debug("SQL query: %s", sql); - log_debug("SQL params: %s", debugParameters); - } - - /* now stash away the SQL STATE if any */ - if (context && sqlstate) - { - AbstractResultContext *ctx = (AbstractResultContext *) context; - - strlcpy(ctx->sqlstate, sqlstate, SQLSTATE_LENGTH); - } - - /* if we get a connection exception, track that */ - if (sqlstate && - strncmp(sqlstate, STR_ERRCODE_CLASS_CONNECTION_EXCEPTION, 2) == 0) - { - pgsql->status = PG_CONNECTION_BAD; - } - - PQclear(result); - clear_results(pgsql); - - /* - * Multi statements might want to ROLLBACK and hold to the open - * connection for a retry step. - */ - if (pgsql->connectionStatementType == PGSQL_CONNECTION_SINGLE_STATEMENT) - { - PQfinish(pgsql->connection); - pgsql->connection = NULL; - } - - return false; - } - - if (parseFun != NULL) - { - (*parseFun)(context, result); - } - - PQclear(result); - clear_results(pgsql); - if (pgsql->connectionStatementType == PGSQL_CONNECTION_SINGLE_STATEMENT) - { - PQfinish(pgsql->connection); - pgsql->connection = NULL; - } - - return true; -} - - -/* - * is_response_ok returns whether the query result is a correct response - * (not an error or failure). - */ -static bool -is_response_ok(PGresult *result) -{ - ExecStatusType resultStatus = PQresultStatus(result); - - return resultStatus == PGRES_SINGLE_TUPLE || resultStatus == PGRES_TUPLES_OK || - resultStatus == PGRES_COMMAND_OK; -} - - -/* - * clear_results consumes results on a connection until NULL is returned. - * If an error is returned it returns false. - */ -static bool -clear_results(PGSQL *pgsql) -{ - PGconn *connection = pgsql->connection; - - /* - * Per Postgres documentation: You should, however, remember to check - * PQnotifies after each PQgetResult or PQexec, to see if any - * notifications came in during the processing of the command. - * - * Before calling clear_results(), we called PQexecParams(). - */ - (void) pgsql_handle_notifications(pgsql); - - while (true) - { - PGresult *result = PQgetResult(connection); - - /* - * Per Postgres documentation: You should, however, remember to check - * PQnotifies after each PQgetResult or PQexec, to see if any - * notifications came in during the processing of the command. - * - * Here, we just called PQgetResult(). - */ - (void) pgsql_handle_notifications(pgsql); - - if (result == NULL) - { - break; - } - - if (!is_response_ok(result)) - { - log_error("Failure from Postgres: %s", PQerrorMessage(connection)); - - PQclear(result); - pgsql_finish(pgsql); - return false; - } - - PQclear(result); - } - - return true; -} - - -/* - * pgsql_handle_notifications check PQnotifies when a PGSQL notificationChannel - * has been set. Then if the parsed notification is from the - * notificationGroupId we set notificationReceived and also log the - * notification. - * - * This allow another part of the code to later know that some notifications - * have been received. - */ -static void -pgsql_handle_notifications(PGSQL *pgsql) -{ - PGconn *connection = pgsql->connection; - PGnotify *notify; - - if (pgsql->notificationProcessFunction == NULL) - { - return; - } - - PQconsumeInput(connection); - while ((notify = PQnotifies(connection)) != NULL) - { - log_trace("pgsql_handle_notifications: \"%s\"", notify->extra); - - if ((*pgsql->notificationProcessFunction)(pgsql->notificationGroupId, - pgsql->notificationNodeId, - notify->relname, - notify->extra)) - { - /* mark that we received some notifications */ - pgsql->notificationReceived = true; - } - - PQfreemem(notify); - PQconsumeInput(connection); - } -} - - -/* - * pgsql_is_in_recovery connects to PostgreSQL and sets the is_in_recovery - * boolean to the result of the SELECT pg_is_in_recovery() query. It returns - * false when something went wrong doing that. - */ -bool -pgsql_is_in_recovery(PGSQL *pgsql, bool *is_in_recovery) -{ - SingleValueResultContext context = { { 0 }, PGSQL_RESULT_BOOL, false }; - char *sql = "SELECT pg_is_in_recovery()"; - - if (!pgsql_execute_with_params(pgsql, sql, 0, NULL, NULL, - &context, &parseSingleValueResult)) - { - /* errors have been logged already */ - return false; - } - - if (!context.parsedOk) - { - log_error("Failed to get result from pg_is_in_recovery()"); - return false; - } - - *is_in_recovery = context.boolVal; - - return true; -} - - -/* - * check_postgresql_settings connects to our local PostgreSQL instance and - * verifies that our minimal viable configuration is in place by running a SQL - * query that looks at the current settings. - */ -bool -pgsql_check_postgresql_settings(PGSQL *pgsql, bool isCitusInstanceKind, - bool *settings_are_ok) -{ - SingleValueResultContext context = { { 0 }, PGSQL_RESULT_BOOL, false }; - const char *sql = - isCitusInstanceKind ? - CHECK_CITUS_NODE_SETTINGS_SQL : CHECK_POSTGRESQL_NODE_SETTINGS_SQL; - - if (!pgsql_execute_with_params(pgsql, sql, 0, NULL, NULL, - &context, &parseSingleValueResult)) - { - /* errors have been logged already */ - return false; - } - - if (!context.parsedOk) - { - /* errors have already been logged */ - return false; - } - - *settings_are_ok = context.boolVal; - - return true; -} - - -/* - * pgsql_check_monitor_settings connects to the given pgsql instance to check - * that pgautofailover is part of shared_preload_libraries. - */ -bool -pgsql_check_monitor_settings(PGSQL *pgsql, bool *settings_are_ok) -{ - SingleValueResultContext context = { { 0 }, PGSQL_RESULT_BOOL, false }; - const char *sql = - "select exists(select 1 from " - "unnest(" - "string_to_array(current_setting('shared_preload_libraries'), ','))" - " as t(name) " - "where trim(name) = 'pgautofailover');"; - - if (!pgsql_execute_with_params(pgsql, sql, 0, NULL, NULL, - &context, &parseSingleValueResult)) - { - /* errors have been logged already */ - return false; - } - - if (!context.parsedOk) - { - /* errors have already been logged */ - return false; - } - - *settings_are_ok = context.boolVal; - - return true; -} - - -/* - * postgres_sprintf_replicationSlotName prints the replication Slot Name to use - * for given nodeId in the given slotName buffer of given size. - */ -bool -postgres_sprintf_replicationSlotName(int64_t nodeId, char *slotName, int size) -{ - int bytesWritten = - sformat(slotName, size, "%s_%" PRId64, - REPLICATION_SLOT_NAME_DEFAULT, - nodeId); - - return bytesWritten <= size; -} - - -/* - * pgsql_set_synchronous_standby_names set synchronous_standby_names on the - * local Postgres to the value computed on the pg_auto_failover monitor. - */ -bool -pgsql_set_synchronous_standby_names(PGSQL *pgsql, - char *synchronous_standby_names) -{ - char quoted[BUFSIZE] = { 0 }; - GUC setting = { "synchronous_standby_names", quoted }; - - if (sformat(quoted, BUFSIZE, "'%s'", synchronous_standby_names) >= BUFSIZE) - { - log_error("Failed to apply the synchronous_standby_names value \"%s\": " - "pg_autoctl supports values up to %d bytes and this one " - "requires %lu bytes", - synchronous_standby_names, - BUFSIZE, - (unsigned long) strlen(synchronous_standby_names)); - return false; - } - - return pgsql_alter_system_set(pgsql, setting); -} - - -/* - * pgsql_replication_slot_maintain advances the current confirmed position of - * the given replication slot up to the given LSN position, create the - * replication slot if it does not exist yet, and remove the slots that exist - * in Postgres but are ommited in the given array of slots. - */ -typedef struct ReplicationSlotMaintainContext -{ - char sqlstate[SQLSTATE_LENGTH]; - char operation[NAMEDATALEN]; - char slotName[BUFSIZE]; - char lsn[PG_LSN_MAXLENGTH]; - bool parsedOK; -} ReplicationSlotMaintainContext; - - -/* - * pgsql_replication_slot_exists checks that a replication slot with the given - * slotName exists on the Postgres server. - */ -bool -pgsql_replication_slot_exists(PGSQL *pgsql, const char *slotName, - bool *slotExists) -{ - SingleValueResultContext context = { { 0 }, PGSQL_RESULT_BOOL, false }; - char *sql = "SELECT 1 FROM pg_replication_slots WHERE slot_name = $1"; - int paramCount = 1; - Oid paramTypes[1] = { NAMEOID }; - const char *paramValues[1] = { slotName }; - - if (!pgsql_execute_with_params(pgsql, sql, - paramCount, paramTypes, paramValues, - &context, &fetchedRows)) - { - /* errors have already been logged */ - return false; - } - - if (!context.parsedOk) - { - log_error("Failed to check if the replication slot \"%s\" exists", - slotName); - return false; - } - - /* we receive 0 rows in the result when the slot does not exist yet */ - *slotExists = context.intVal == 1; - - return true; -} - - -/* - * pgsql_create_replication_slot tries to create a replication slot on the - * database identified by a connection string. It's implemented as CREATE IF - * NOT EXISTS so that it's idempotent and can be retried easily. - */ -bool -pgsql_create_replication_slot(PGSQL *pgsql, const char *slotName) -{ - ReplicationSlotMaintainContext context = { 0 }; - char *sql = - "SELECT 'create', slot_name, lsn " - " FROM pg_create_physical_replication_slot($1) " - " WHERE NOT EXISTS " - " (SELECT 1 FROM pg_replication_slots WHERE slot_name = $1)"; - const Oid paramTypes[1] = { TEXTOID }; - const char *paramValues[1] = { slotName }; - - log_trace("pgsql_create_replication_slot"); - - /* - * parseReplicationSlotMaintain will log_info() the replication slot - * creation if it happens. When the slot already exists we return 0 row and - * remain silent about it. - */ - return pgsql_execute_with_params(pgsql, sql, 1, paramTypes, paramValues, - &context, parseReplicationSlotMaintain); -} - - -/* - * pgsql_drop_replication_slot drops a given replication slot. - */ -bool -pgsql_drop_replication_slot(PGSQL *pgsql, const char *slotName) -{ - char *sql = - "SELECT pg_drop_replication_slot(slot_name) " - " FROM pg_replication_slots " - " WHERE slot_name = $1"; - Oid paramTypes[1] = { TEXTOID }; - const char *paramValues[1] = { slotName }; - - log_info("Drop replication slot \"%s\"", slotName); - - return pgsql_execute_with_params(pgsql, sql, - 1, paramTypes, paramValues, NULL, NULL); -} - - -/* - * BuildNodesArrayValues build the SQL expression to use in a FROM clause to - * represent the list of other standby nodes from the given nodeArray. - * - * Such a list looks either like: - * - * VALUES($1, $2::pg_lsn), ($3, $4) - * - * or for an empty set (e.g. when we're the only standby): - * - * SELECT id, lsn - * FROM (values (null::int, null::pg_lsn)) as t(id, lsn) - * WHERE false - * - * We actually need to provide an empty set (0 rows) with columns of the - * expected data types so that we can join against the existing replication - * slots and drop them. If the set is empty, we drop all the slots. - * - * We return how many parameters we filled in paramTypes and paramValues from - * the nodeArray. - */ -#define NODEID_MAX_LENGTH 20 /* should allow for bigint digits */ - -typedef struct nodesArraysValuesParams -{ - int count; - Oid types[NODE_ARRAY_MAX_COUNT * 2]; - char *values[NODE_ARRAY_MAX_COUNT * 2]; - - /* - * Pre-allocate arrays for the data separately from the values array, which - * needs to be a (const char **) thing rather than a (char [][]) thing, - * because of the pgsql_execute_with_params and libpq APIs. - */ - char nodeIds[NODE_ARRAY_MAX_COUNT][NODEID_MAX_LENGTH]; - char lsns[NODE_ARRAY_MAX_COUNT][PG_LSN_MAXLENGTH]; -} nodesArraysValuesParams; - - -static bool -BuildNodesArrayValues(NodeAddressArray *nodeArray, - nodesArraysValuesParams *sqlParams, - PQExpBuffer values) -{ - int nodeIndex = 0; - int paramIndex = 0; - - /* when we didn't find any node to process, return our empty set */ - if (nodeArray->count == 0) - { - appendPQExpBufferStr( - values, - "SELECT id, lsn " - "FROM (values (null::int, null::pg_lsn)) as t(id, lsn) " - "where false"); - - return true; - } - - /* we start the VALUES subquery with the values SQL keyword */ - appendPQExpBufferStr(values, "values "); - - /* - * Build a SQL VALUES statement for every other node registered in the - * system, so that we can maintain their LSN position locally on a standby - * server. - */ - for (nodeIndex = 0; nodeIndex < nodeArray->count; nodeIndex++) - { - NodeAddress *node = &(nodeArray->nodes[nodeIndex]); - char *nodeIdString = intToString(node->nodeId).strValue; - - int idParamIndex = paramIndex; - int lsnParamIndex = paramIndex + 1; - - sqlParams->types[idParamIndex] = INT8OID; - strlcpy(sqlParams->nodeIds[nodeIndex], nodeIdString, NODEID_MAX_LENGTH); - - /* store the (char *) pointer to the data in values */ - sqlParams->values[idParamIndex] = sqlParams->nodeIds[nodeIndex]; - - sqlParams->types[lsnParamIndex] = LSNOID; - strlcpy(sqlParams->lsns[nodeIndex], node->lsn, PG_LSN_MAXLENGTH); - - /* store the (char *) pointer to the data in values */ - sqlParams->values[lsnParamIndex] = sqlParams->lsns[nodeIndex]; - - appendPQExpBuffer(values, - "%s($%d, $%d%s)", - paramIndex == 0 ? "" : ",", - - /* we begin at $1 here: intentional off-by-one */ - idParamIndex + 1, lsnParamIndex + 1, - - /* cast only the first row */ - paramIndex == 0 ? "::pg_lsn" : ""); - - /* prepare next round */ - paramIndex += 2; - } - - /* count how many parameters where appended to the VALUES() parts */ - sqlParams->count = paramIndex; - - return true; -} - - -/* - * pgsql_replication_slot_create_and_drop drops replication slots that belong - * to nodes that have been removed, and creates replication slots for nodes - * that have been newly registered. We call that function on the primary, where - * the slots are maintained by the replication protocol. - * - * On the standby nodes, we advance the slots ourselves and use the other - * function pgsql_replication_slot_maintain which is complete (create, drop, - * advance). - */ -bool -pgsql_replication_slot_create_and_drop(PGSQL *pgsql, NodeAddressArray *nodeArray) -{ - PQExpBuffer query = createPQExpBuffer(); - PQExpBuffer values = createPQExpBuffer(); - - /* *INDENT-OFF* */ - char *sqlTemplate = - /* - * We could simplify the writing of this query, but we prefer that it - * looks as much as possible like the query used in - * pgsql_replication_slot_maintain() so that we can maintain both - * easily. - */ - "WITH nodes(slot_name, lsn) as (" - " SELECT '" REPLICATION_SLOT_NAME_DEFAULT "_' || id, lsn" - " FROM (%s) as sb(id, lsn) " - "), \n" - "dropped as (" - " SELECT slot_name, pg_drop_replication_slot(slot_name) " - " FROM pg_replication_slots pgrs LEFT JOIN nodes USING(slot_name) " - " WHERE nodes.slot_name IS NULL " - " AND ( slot_name ~ '" REPLICATION_SLOT_NAME_PATTERN "' " - " OR slot_name ~ '" REPLICATION_SLOT_NAME_DEFAULT "' )" - " AND not active" - " AND slot_type = 'physical'" - "), \n" - "created as (" - "SELECT c.slot_name, c.lsn " - " FROM nodes LEFT JOIN pg_replication_slots pgrs USING(slot_name), " - " LATERAL pg_create_physical_replication_slot(slot_name, true) c" - " WHERE pgrs.slot_name IS NULL " - ") \n" - "SELECT 'create', slot_name, lsn FROM created " - " union all " - "SELECT 'drop', slot_name, NULL::pg_lsn FROM dropped"; - /* *INDENT-ON* */ - - nodesArraysValuesParams sqlParams = { 0 }; - ReplicationSlotMaintainContext context = { 0 }; - - if (!BuildNodesArrayValues(nodeArray, &sqlParams, values)) - { - /* errors have already been logged */ - destroyPQExpBuffer(query); - destroyPQExpBuffer(values); - - return false; - } - - /* add the computed ($1,$2), ... string to the query "template" */ - appendPQExpBuffer(query, sqlTemplate, values->data); - - bool success = - pgsql_execute_with_params(pgsql, - query->data, - sqlParams.count, - sqlParams.types, - (const char **) sqlParams.values, - &context, - parseReplicationSlotMaintain); - - destroyPQExpBuffer(query); - destroyPQExpBuffer(values); - - return success; -} - - -/* - * pgsql_replication_slot_maintain creates, drops, and advance replication - * slots that belong to other standby nodes. We call that function on the - * standby nodes, where the slots are maintained manually just in case we need - * them at failover. - */ -bool -pgsql_replication_slot_maintain(PGSQL *pgsql, NodeAddressArray *nodeArray) -{ - PQExpBuffer query = createPQExpBuffer(); - PQExpBuffer values = createPQExpBuffer(); - - /* *INDENT-OFF* */ - char *sqlTemplate = - "WITH nodes(slot_name, lsn) as (" - " SELECT '" REPLICATION_SLOT_NAME_DEFAULT "_' || id, lsn" - " FROM (%s) as sb(id, lsn) " - "), \n" - "dropped as (" - " SELECT slot_name, pg_drop_replication_slot(slot_name) " - " FROM pg_replication_slots pgrs LEFT JOIN nodes USING(slot_name) " - " WHERE nodes.slot_name IS NULL " - " AND slot_name ~ '" REPLICATION_SLOT_NAME_PATTERN "' " - " AND not active" - " AND slot_type = 'physical'" - "), \n" - "advanced as (" - "SELECT a.slot_name, a.end_lsn" - " FROM pg_replication_slots s JOIN nodes USING(slot_name), " - " LATERAL pg_replication_slot_advance(slot_name, lsn) a" - " WHERE nodes.lsn <> '0/0' and nodes.lsn >= s.restart_lsn " - " and not s.active " - "), \n" - "created as (" - "SELECT c.slot_name, c.lsn " - " FROM nodes LEFT JOIN pg_replication_slots pgrs USING(slot_name), " - " LATERAL pg_create_physical_replication_slot(slot_name, true) c" - " WHERE pgrs.slot_name IS NULL " - ") \n" - "SELECT 'create', slot_name, lsn FROM created " - " union all " - "SELECT 'drop', slot_name, NULL::pg_lsn FROM dropped " - " union all " - "SELECT 'advance', slot_name, end_lsn FROM advanced "; - /* *INDENT-ON* */ - - nodesArraysValuesParams sqlParams = { 0 }; - ReplicationSlotMaintainContext context = { 0 }; - - if (!BuildNodesArrayValues(nodeArray, &sqlParams, values)) - { - /* errors have already been logged */ - destroyPQExpBuffer(query); - destroyPQExpBuffer(values); - - return false; - } - - /* add the computed ($1,$2), ... string to the query "template" */ - appendPQExpBuffer(query, sqlTemplate, values->data); - - bool success = - pgsql_execute_with_params(pgsql, - query->data, - sqlParams.count, - sqlParams.types, - (const char **) sqlParams.values, - &context, - parseReplicationSlotMaintain); - - destroyPQExpBuffer(query); - destroyPQExpBuffer(values); - - return success; -} - - -/* - * parseReplicationSlotMaintain parses the result from a PostgreSQL query - * fetching two columns from pg_stat_replication: sync_state and currentLSN. - */ -static void -parseReplicationSlotMaintain(void *ctx, PGresult *result) -{ - int rowNumber = 0; - ReplicationSlotMaintainContext *context = - (ReplicationSlotMaintainContext *) ctx; - - if (PQnfields(result) != 3) - { - log_error("Query returned %d columns, expected 3", PQnfields(result)); - context->parsedOK = false; - return; - } - - for (rowNumber = 0; rowNumber < PQntuples(result); rowNumber++) - { - /* operation and slotName can't be NULL given how the SQL is built */ - char *operation = PQgetvalue(result, rowNumber, 0); - char *slotName = PQgetvalue(result, rowNumber, 1); - char *lsn = PQgetisnull(result, rowNumber, 2) ? "" - : PQgetvalue(result, rowNumber, 2); - - /* adding or removing another standby node is worthy of a log line */ - if (strcmp(operation, "create") == 0) - { - log_info("Creating replication slot \"%s\"", slotName); - } - else if (strcmp(operation, "drop") == 0) - { - log_info("Dropping replication slot \"%s\"", slotName); - } - else - { - log_debug("parseReplicationSlotMaintain: %s %s %s", - operation, slotName, lsn); - } - } - - context->parsedOK = true; -} - - -/* - * pgsql_disable_synchronous_replication disables synchronous replication - * in Postgres such that writes do not block if there is no replica. - */ -bool -pgsql_disable_synchronous_replication(PGSQL *pgsql) -{ - GUC setting = { "synchronous_standby_names", "''" }; - char *cancelBlockedStatementsCommand = - "SELECT pg_cancel_backend(pid) " - " FROM pg_stat_activity " - " WHERE wait_event = 'SyncRep'"; - - log_info("Disabling synchronous replication"); - - if (!pgsql_alter_system_set(pgsql, setting)) - { - return false; - } - - log_debug("Unblocking commands waiting for synchronous replication"); - - if (!pgsql_execute(pgsql, cancelBlockedStatementsCommand)) - { - return false; - } - - return true; -} - - -/* - * pgsql_set_default_transaction_mode_read_only makes it so that the server - * won't be a target of a connection string requiring target_session_attrs - * read-write by issuing ALTER SYSTEM SET transaction_mode_read_only TO on; - * - */ -bool -pgsql_set_default_transaction_mode_read_only(PGSQL *pgsql) -{ - GUC setting = { "default_transaction_read_only", "'on'" }; - - log_info("Setting default_transaction_read_only to on"); - - return pgsql_alter_system_set(pgsql, setting); -} - - -/* - * pgsql_set_default_transaction_mode_read_write makes it so that the server - * can be a target of a connection string requiring target_session_attrs - * read-write by issuing ALTER SYSTEM SET transaction_mode_read_only TO off; - * - */ -bool -pgsql_set_default_transaction_mode_read_write(PGSQL *pgsql) -{ - GUC setting = { "default_transaction_read_only", "'off'" }; - - log_info("Setting default_transaction_read_only to off"); - - return pgsql_alter_system_set(pgsql, setting); -} - - -/* - * pgsql_checkpoint runs a CHECKPOINT command on postgres to trigger a checkpoint. - */ -bool -pgsql_checkpoint(PGSQL *pgsql) -{ - return pgsql_execute(pgsql, "CHECKPOINT"); -} - - -/* - * pgsql_alter_system_set runs an ALTER SYSTEM SET ... command on Postgres - * to globally set a GUC and then runs pg_reload_conf() to make existing - * sessions reload it. - */ -static bool -pgsql_alter_system_set(PGSQL *pgsql, GUC setting) -{ - char command[BUFSIZE]; - - sformat(command, sizeof(command), - "ALTER SYSTEM SET %s TO %s", setting.name, setting.value); - - if (!pgsql_execute(pgsql, command)) - { - log_error("Failed to set \"%s\" to \"%s\" with ALTER SYSTEM, " - "see above for details", - setting.name, setting.value); - return false; - } - - if (!pgsql_reload_conf(pgsql)) - { - log_error("Failed to reload Postgres config after ALTER SYSTEM " - "to set \"%s\" to \"%s\".", - setting.name, setting.value); - return false; - } - - return true; -} - - -/* - * pgsql_reset_primary_conninfo issues the following SQL commands: - * - * ALTER SYSTEM RESET primary_conninfo; - * ALTER SYSTEM RESET primary_slot_name; - * - * That's necessary to clean-up the replication settings that pg_basebackup - * puts in place in postgresql.auto.conf in Postgres 12. We don't reload the - * configuration after the RESET in that case, because Postgres 12 requires a - * restart to apply the new setting value anyway. - */ -bool -pgsql_reset_primary_conninfo(PGSQL *pgsql) -{ - char *reset_primary_conninfo = "ALTER SYSTEM RESET primary_conninfo"; - char *reset_primary_slot_name = "ALTER SYSTEM RESET primary_slot_name"; - - /* ALTER SYSTEM cannot run inside a transaction block */ - if (!pgsql_execute(pgsql, reset_primary_conninfo)) - { - return false; - } - - if (!pgsql_execute(pgsql, reset_primary_slot_name)) - { - return false; - } - - return true; -} - - -/* - * pgsql_reload_conf causes open sessions to reload the PostgreSQL configuration - * files. - */ -bool -pgsql_reload_conf(PGSQL *pgsql) -{ - char *sql = "SELECT pg_reload_conf()"; - - log_info("Reloading Postgres configuration and HBA rules"); - - return pgsql_execute(pgsql, sql); -} - - -/* - * pgsql_get_hba_file_path gets the value of the hba_file setting in - * Postgres or returns false if a failure occurred. The value is copied to - * the hbaFilePath pointer. - */ -bool -pgsql_get_hba_file_path(PGSQL *pgsql, char *hbaFilePath, int maxPathLength) -{ - char *configValue = NULL; - - if (!pgsql_get_current_setting(pgsql, "hba_file", &configValue)) - { - /* pgsql_get_current_setting logs a relevant error */ - return false; - } - - int hbaFilePathLength = strlcpy(hbaFilePath, configValue, maxPathLength); - - if (hbaFilePathLength >= maxPathLength) - { - log_error("The hba_file \"%s\" returned by postgres is %d characters, " - "the maximum supported by pg_autoctl is %d characters", - configValue, hbaFilePathLength, maxPathLength); - free(configValue); - return false; - } - - free(configValue); - - return true; -} - - -/* - * pgsql_get_current_setting gets the value of a GUC in Postgres by running - * SELECT current_setting($settingName), or returns false if a failure occurred. - * - * If getting the value was successful, currentValue will point to a copy of the - * value which should be freed by the caller. - */ -static bool -pgsql_get_current_setting(PGSQL *pgsql, char *settingName, char **currentValue) -{ - SingleValueResultContext context = { 0 }; - char *sql = "SELECT current_setting($1)"; - int paramCount = 1; - Oid paramTypes[1] = { TEXTOID }; - const char *paramValues[1] = { settingName }; - - context.resultType = PGSQL_RESULT_STRING; - - if (!pgsql_execute_with_params(pgsql, sql, paramCount, paramTypes, paramValues, - &context, &parseSingleValueResult)) - { - /* errors have already been logged */ - return false; - } - - if (!context.parsedOk) - { - log_error("Failed to get result from current_setting('%s')", settingName); - return false; - } - - *currentValue = context.strVal; - - return true; -} - - -/* - * pgsql_create_database issues a CREATE DATABASE statement. - */ -bool -pgsql_create_database(PGSQL *pgsql, const char *dbname, const char *owner) -{ - char command[BUFSIZE]; - char *escapedDBName, *escapedOwner; - - /* open a connection upfront since it is needed by PQescape functions */ - PGconn *connection = pgsql_open_connection(pgsql); - if (connection == NULL) - { - /* error message was logged in pgsql_open_connection */ - return false; - } - - /* escape the dbname */ - escapedDBName = PQescapeIdentifier(connection, dbname, strlen(dbname)); - if (escapedDBName == NULL) - { - log_error("Failed to create database \"%s\": %s", dbname, - PQerrorMessage(connection)); - pgsql_finish(pgsql); - return false; - } - - /* escape the username */ - escapedOwner = PQescapeIdentifier(connection, owner, strlen(owner)); - if (escapedOwner == NULL) - { - log_error("Failed to create database \"%s\": %s", dbname, - PQerrorMessage(connection)); - PQfreemem(escapedDBName); - pgsql_finish(pgsql); - return false; - } - - /* now build the SQL command */ - sformat(command, BUFSIZE, - "CREATE DATABASE %s WITH OWNER %s", - escapedDBName, - escapedOwner); - - log_debug("Running command on Postgres: %s;", command); - - PQfreemem(escapedDBName); - PQfreemem(escapedOwner); - - PGresult *result = PQexec(connection, command); - - if (!is_response_ok(result)) - { - /* - * Check if we have a duplicate_database (42P04) error, in which case - * it means the user has already been created, accept that as a - * non-error, only inform about the situation. - */ - char *sqlstate = PQresultErrorField(result, PG_DIAG_SQLSTATE); - - if (strcmp(sqlstate, STR_ERRCODE_DUPLICATE_DATABASE) == 0) - { - log_info("The database \"%s\" already exists, skipping.", dbname); - } - else - { - log_error("Failed to create database \"%s\"[%s]: %s", - dbname, sqlstate, PQerrorMessage(connection)); - PQclear(result); - clear_results(pgsql); - pgsql_finish(pgsql); - return false; - } - } - - PQclear(result); - clear_results(pgsql); - if (pgsql->connectionStatementType == PGSQL_CONNECTION_SINGLE_STATEMENT) - { - pgsql_finish(pgsql); - } - - return true; -} - - -/* - * pgsql_create_extension issues a CREATE EXTENSION statement. - */ -bool -pgsql_create_extension(PGSQL *pgsql, const char *name) -{ - char command[BUFSIZE]; - - /* open a connection upfront since it is needed by PQescape functions */ - PGconn *connection = pgsql_open_connection(pgsql); - if (connection == NULL) - { - /* error message was logged in pgsql_open_connection */ - return false; - } - - /* escape the dbname */ - char *escapedIdentifier = PQescapeIdentifier(connection, name, strlen(name)); - if (escapedIdentifier == NULL) - { - log_error("Failed to create extension \"%s\": %s", name, - PQerrorMessage(connection)); - pgsql_finish(pgsql); - return false; - } - - /* now build the SQL command */ - sformat(command, BUFSIZE, "CREATE EXTENSION IF NOT EXISTS %s CASCADE", - escapedIdentifier); - PQfreemem(escapedIdentifier); - log_debug("Running command on Postgres: %s;", command); - - PGresult *result = PQexec(connection, command); - - if (!is_response_ok(result)) - { - /* - * Check if we have a duplicate_object (42710) error, in which case - * it means the user has already been created, accept that as a - * non-error, only inform about the situation. - */ - char *sqlstate = PQresultErrorField(result, PG_DIAG_SQLSTATE); - - log_error("Failed to create extension \"%s\"[%s]: %s", - name, sqlstate, PQerrorMessage(connection)); - PQclear(result); - clear_results(pgsql); - pgsql_finish(pgsql); - return false; - } - - PQclear(result); - clear_results(pgsql); - if (pgsql->connectionStatementType == PGSQL_CONNECTION_SINGLE_STATEMENT) - { - pgsql_finish(pgsql); - } - - return true; -} - - -/* - * pgsql_create_user creates a user with the given settings. - * - * Unlike most functions this function does opens a connection itself - * because it has some specific requirements around logging, error handling - * and escaping. - */ -bool -pgsql_create_user(PGSQL *pgsql, const char *userName, const char *password, - bool login, bool superuser, bool replication, int connlimit) -{ - /* open a connection upfront since it is needed by PQescape functions */ - PGconn *connection = pgsql_open_connection(pgsql); - if (connection == NULL) - { - /* error message was logged in pgsql_open_connection */ - return false; - } - - /* escape the username */ - PQExpBuffer query = createPQExpBuffer(); - char *escapedIdentifier = PQescapeIdentifier(connection, userName, strlen(userName)); - if (escapedIdentifier == NULL) - { - log_error("Failed to create user \"%s\": %s", userName, - PQerrorMessage(connection)); - pgsql_finish(pgsql); - return false; - } - - appendPQExpBuffer(query, "CREATE USER %s", escapedIdentifier); - PQfreemem(escapedIdentifier); - - if (login || superuser || replication || password) - { - appendPQExpBufferStr(query, " WITH"); - } - if (login) - { - appendPQExpBufferStr(query, " LOGIN"); - } - if (superuser) - { - appendPQExpBufferStr(query, " SUPERUSER"); - } - if (replication) - { - appendPQExpBufferStr(query, " REPLICATION"); - } - if (connlimit > -1) - { - appendPQExpBuffer(query, " CONNECTION LIMIT %d", connlimit); - } - if (password) - { - /* show the statement before we append the password */ - log_debug("%s PASSWORD '*****';", query->data); - - escapedIdentifier = PQescapeLiteral(connection, password, strlen(password)); - if (escapedIdentifier == NULL) - { - log_error("Failed to create user \"%s\": %s", userName, - PQerrorMessage(connection)); - PQfreemem(escapedIdentifier); - pgsql_finish(pgsql); - destroyPQExpBuffer(query); - return false; - } - - appendPQExpBuffer(query, " PASSWORD %s", escapedIdentifier); - PQfreemem(escapedIdentifier); - } - else - { - log_debug("%s;", query->data); - } - - /* memory allocation could have failed while building string */ - if (PQExpBufferBroken(query)) - { - log_error("Failed to allocate memory"); - destroyPQExpBuffer(query); - pgsql_finish(pgsql); - return false; - } - - /* - * Set the libpq notice receiver to integrate notifications as debug - * message, because when dealing with the citus extension those messages - * are not that interesting to our pg_autoctl users frankly: - * - * NOTICE: not propagating CREATE ROLE/USER commands to worker nodes - * HINT: Connect to worker nodes directly... - */ - PQnoticeProcessor previousNoticeProcessor = - PQsetNoticeProcessor(connection, &pgAutoCtlDebugNoticeProcessor, NULL); - - PGresult *result = PQexec(connection, query->data); - destroyPQExpBuffer(query); - - if (!is_response_ok(result)) - { - /* - * Check if we have a duplicate_object (42710) error, in which case - * it means the user has already been created, accept that as a - * non-error, only inform about the situation. - */ - char *sqlstate = PQresultErrorField(result, PG_DIAG_SQLSTATE); - - if (strcmp(sqlstate, STR_ERRCODE_DUPLICATE_OBJECT) == 0) - { - log_info("The user \"%s\" already exists, skipping.", userName); - } - else - { - log_error("Failed to create user \"%s\"[%s]: %s", - userName, sqlstate, PQerrorMessage(connection)); - PQclear(result); - clear_results(pgsql); - pgsql_finish(pgsql); - return false; - } - } - - PQclear(result); - clear_results(pgsql); - - if (pgsql->connectionStatementType == PGSQL_CONNECTION_SINGLE_STATEMENT) - { - pgsql_finish(pgsql); - } - else - { - /* restore the normal notice message processing, if needed. */ - PQsetNoticeProcessor(connection, previousNoticeProcessor, NULL); - } - - return true; -} - - -/* - * pgsql_has_replica returns whether a replica with the given username is active. - */ -bool -pgsql_has_replica(PGSQL *pgsql, char *userName, bool *hasReplica) -{ - SingleValueResultContext context = { { 0 }, PGSQL_RESULT_BOOL, false }; - - /* - * Check whether there is an entry in pg_stat_replication, which means - * there is either a pg_basebackup or streaming replica active. In either - * case, it means there is a replica that recently communicated with the - * postgres server, which is all we care about for the purpose of this - * function. - */ - char *sql = - "SELECT EXISTS (SELECT 1 FROM pg_stat_replication WHERE usename = $1)"; - - const Oid paramTypes[1] = { TEXTOID }; - const char *paramValues[1] = { userName }; - int paramCount = 1; - - if (!pgsql_execute_with_params(pgsql, sql, paramCount, paramTypes, paramValues, - &context, &parseSingleValueResult)) - { - /* errors have already been logged */ - return false; - } - - if (!context.parsedOk) - { - log_error("Failed to find pg_stat_replication"); - return false; - } - - *hasReplica = context.boolVal; - - return true; -} - - -/* - * hostname_from_uri parses a PostgreSQL connection string URI and returns - * whether the URL was successfully parsed. - */ -bool -hostname_from_uri(const char *pguri, - char *hostname, int maxHostLength, int *port) -{ - int found = 0; - char *errmsg; - PQconninfoOption *conninfo, *option; - - conninfo = PQconninfoParse(pguri, &errmsg); - if (conninfo == NULL) - { - log_error("Failed to parse pguri \"%s\": %s", pguri, errmsg); - PQfreemem(errmsg); - return false; - } - - for (option = conninfo; option->keyword != NULL; option++) - { - if (strcmp(option->keyword, "host") == 0 || - strcmp(option->keyword, "hostaddr") == 0) - { - if (option->val) - { - int hostNameLength = strlcpy(hostname, option->val, maxHostLength); - - if (hostNameLength >= maxHostLength) - { - log_error( - "The URL \"%s\" contains a hostname of %d characters, " - "the maximum supported by pg_autoctl is %d characters", - option->val, hostNameLength, maxHostLength); - PQconninfoFree(conninfo); - return false; - } - - ++found; - } - } - - if (strcmp(option->keyword, "port") == 0) - { - if (option->val) - { - /* we expect a single port number in a monitor's URI */ - if (!stringToInt(option->val, port)) - { - log_error("Failed to parse port number : %s", option->val); - - PQconninfoFree(conninfo); - return false; - } - ++found; - } - else - { - *port = POSTGRES_PORT; - } - } - - if (found == 2) - { - break; - } - } - PQconninfoFree(conninfo); - - return true; -} - - -/* - * validate_connection_string takes a connection string and parses it with - * libpq, varifying that it's well formed and usable. - */ -bool -validate_connection_string(const char *connectionString) -{ - char *errorMessage = NULL; - - int length = strlen(connectionString); - if (length >= MAXCONNINFO) - { - log_error("Connection string \"%s\" is %d " - "characters, the maximum supported by pg_autoctl is %d", - connectionString, length, MAXCONNINFO); - return false; - } - - PQconninfoOption *connInfo = PQconninfoParse(connectionString, &errorMessage); - if (connInfo == NULL) - { - log_error("Failed to parse connection string \"%s\": %s ", - connectionString, errorMessage); - PQfreemem(errorMessage); - return false; - } - - PQconninfoFree(connInfo); - - return true; -} - - -/* - * pgsql_get_postgres_metadata returns several bits of information that we need - * to take decisions in the rest of the code: - * - * - pg_is_in_recovery (primary or standby, as expected?) - * - sync_state from pg_stat_replication when a primary - * - current_lsn from the server - * - pg_control_version - * - catalog_version_no - * - system_identifier - * - * With those metadata we can then check our expectations and take decisions in - * some cases. We can obtain all the metadata that we need easily enough in a - * single SQL query, so that's what we do. - */ -typedef struct PgMetadata -{ - char sqlstate[6]; - bool parsedOk; - bool pg_is_in_recovery; - char syncState[PGSR_SYNC_STATE_MAXLENGTH]; - char currentLSN[PG_LSN_MAXLENGTH]; - PostgresControlData control; -} PgMetadata; - - -bool -pgsql_get_postgres_metadata(PGSQL *pgsql, - bool *pg_is_in_recovery, - char *pgsrSyncState, - char *currentLSN, - PostgresControlData *control) -{ - PgMetadata context = { 0 }; - - /* *INDENT-OFF* */ - char *sql = - /* - * Make it so that we still have the current WAL LSN even in the case - * where there's no replication slot in use by any standby. - * - * When on the primary, we might have multiple standby nodes connected. - * We're good when at least one of them is either 'sync' or 'quorum'. - * We don't check individual replication slots, we take the "best" one - * and report that. - */ - "select pg_is_in_recovery()," - " coalesce(rep.sync_state, '') as sync_state," - " case when pg_is_in_recovery()" - " then coalesce(pg_last_wal_receive_lsn(), pg_last_wal_replay_lsn())" - " else pg_current_wal_flush_lsn()" - " end as current_lsn," - " pg_control_version, catalog_version_no, system_identifier," - " case when pg_is_in_recovery()" - " then (select received_tli from pg_stat_wal_receiver)" - " else (select timeline_id from pg_control_checkpoint()) " - " end as timeline_id " - " from (values(1)) as dummy" - " full outer join" - " (select pg_control_version, catalog_version_no, system_identifier " - " from pg_control_system()" - " )" - " as control on true" - " full outer join" - " (" - " select sync_state" - " from pg_replication_slots slot" - " join pg_stat_replication rep" - " on rep.pid = slot.active_pid" - " where slot_name ~ '" REPLICATION_SLOT_NAME_PATTERN "' " - " or slot_name = '" REPLICATION_SLOT_NAME_DEFAULT "' " - "order by case sync_state " - " when 'quorum' then 4 " - " when 'sync' then 3 " - " when 'potential' then 2 " - " when 'async' then 1 " - " else 0 end " - " desc limit 1" - " ) " - "as rep on true"; - /* *INDENT-ON* */ - - if (!pgsql_execute_with_params(pgsql, sql, 0, NULL, NULL, - &context, &parsePgMetadata)) - { - /* errors have been logged already */ - return false; - } - - if (!context.parsedOk) - { - log_error("Failed to parse the Postgres metadata"); - return false; - } - - *pg_is_in_recovery = context.pg_is_in_recovery; - - /* the last two metadata items are opt-in */ - if (pgsrSyncState != NULL) - { - strlcpy(pgsrSyncState, context.syncState, PGSR_SYNC_STATE_MAXLENGTH); - } - - if (currentLSN != NULL) - { - strlcpy(currentLSN, context.currentLSN, PG_LSN_MAXLENGTH); - } - - /* overwrite the Control Data fetched from the query */ - *control = context.control; - - pgsql_finish(pgsql); - - return true; -} - - -/* - * parsePgMetadata parses the result from a PostgreSQL query fetching - * two columns from pg_stat_replication: sync_state and currentLSN. - */ -static void -parsePgMetadata(void *ctx, PGresult *result) -{ - PgMetadata *context = (PgMetadata *) ctx; - char *value; - - if (PQnfields(result) != 7) - { - log_error("Query returned %d columns, expected 7", PQnfields(result)); - context->parsedOk = false; - return; - } - - if (PQntuples(result) != 1) - { - log_error("Query returned %d rows, expected 1", PQntuples(result)); - context->parsedOk = false; - return; - } - - context->pg_is_in_recovery = strcmp(PQgetvalue(result, 0, 0), "t") == 0; - - if (!PQgetisnull(result, 0, 1)) - { - value = PQgetvalue(result, 0, 1); - - strlcpy(context->syncState, value, PGSR_SYNC_STATE_MAXLENGTH); - } - else - { - context->syncState[0] = '\0'; - } - - if (!PQgetisnull(result, 0, 2)) - { - value = PQgetvalue(result, 0, 2); - - strlcpy(context->currentLSN, value, PG_LSN_MAXLENGTH); - } - else - { - context->currentLSN[0] = '\0'; - } - - value = PQgetvalue(result, 0, 3); - if (!stringToUInt(value, &(context->control.pg_control_version))) - { - log_error("Failed to parse pg_control_version \"%s\"", value); - context->parsedOk = true; - return; - } - - value = PQgetvalue(result, 0, 4); - if (!stringToUInt(value, &(context->control.catalog_version_no))) - { - log_error("Failed to parse catalog_version_no \"%s\"", value); - context->parsedOk = true; - return; - } - - value = PQgetvalue(result, 0, 5); - if (!stringToUInt64(value, &(context->control.system_identifier))) - { - log_error("Failed to parse system_identifier \"%s\"", value); - context->parsedOk = true; - return; - } - - /* - * On a standby node that doesn't have a primary_conninfo then we fail to - * retrieve the received_tli from pg_stat_wal_receiver. We encode the NULL - * we get in that case with a zero, which is not a value we expect. - */ - if (PQgetisnull(result, 0, 6)) - { - context->control.timeline_id = 0; - } - else - { - value = PQgetvalue(result, 0, 6); - if (!stringToUInt(value, &(context->control.timeline_id))) - { - log_error("Failed to parse timeline_id \"%s\"", value); - context->parsedOk = true; - return; - } - } - - context->parsedOk = true; -} - - -typedef struct PgReachedTargetLSN -{ - char sqlstate[6]; - bool parsedOk; - bool hasReachedLSN; - char currentLSN[PG_LSN_MAXLENGTH]; - bool noRows; -} PgReachedTargetLSN; - - -/* - * pgsql_one_slot_has_reached_target_lsn checks that at least one replication - * slot has reached the given LSN already, using the Postgres system views - * pg_replication_slots and pg_stat_replication on the primary server. - */ -bool -pgsql_one_slot_has_reached_target_lsn(PGSQL *pgsql, - char *targetLSN, - char *currentLSN, - bool *hasReachedLSN) -{ - PgReachedTargetLSN context = { 0 }; - - /* - * We pick the most advanced LSN reached by the pgautofailover replication - * slots, and only consider those that have made it to "sync" or "quorum" - * sync_state already. This function is typically called after sync rep has - * been enabled on the primary. - */ - - /* *INDENT-OFF* */ - char *sql = - " select $1::pg_lsn <= flush_lsn, flush_lsn " - " from pg_replication_slots slot" - " join pg_stat_replication rep" - " on rep.pid = slot.active_pid" - " where ( slot_name ~ '" REPLICATION_SLOT_NAME_PATTERN "' " - " or slot_name = '" REPLICATION_SLOT_NAME_DEFAULT "') " - " and sync_state in ('sync', 'quorum') " - "order by flush_lsn desc limit 1"; - /* *INDENT-ON* */ - - const Oid paramTypes[1] = { LSNOID }; - const char *paramValues[1] = { targetLSN }; - - if (!pgsql_execute_with_params(pgsql, sql, 1, paramTypes, paramValues, - &context, &parsePgReachedTargetLSN)) - { - /* errors have been logged already */ - return false; - } - - if (!context.parsedOk) - { - if (context.noRows) - { - log_warn("No standby nodes are connected at the moment"); - } - else - { - log_error("Failed to fetch current flush_lsn location for " - "connected standby nodes, see above for details"); - } - return false; - } - - *hasReachedLSN = context.hasReachedLSN; - strlcpy(currentLSN, context.currentLSN, PG_LSN_MAXLENGTH); - - return true; -} - - -/* - * pgsql_has_reached_target_lsn calls pg_last_wal_replay_lsn() and compares the - * current LSN on the system to the given targetLSN. - */ -bool -pgsql_has_reached_target_lsn(PGSQL *pgsql, char *targetLSN, - char *currentLSN, bool *hasReachedLSN) -{ - PgReachedTargetLSN context = { 0 }; - char *sql = - "SELECT $1::pg_lsn <= pg_last_wal_replay_lsn(), " - " pg_last_wal_replay_lsn()"; - - const Oid paramTypes[1] = { LSNOID }; - const char *paramValues[1] = { targetLSN }; - - if (!pgsql_execute_with_params(pgsql, sql, 1, paramTypes, paramValues, - &context, &parsePgReachedTargetLSN)) - { - /* errors have been logged already */ - return false; - } - - if (!context.parsedOk) - { - log_error("Failed to get result from pg_last_wal_replay_lsn()"); - return false; - } - - *hasReachedLSN = context.hasReachedLSN; - strlcpy(currentLSN, context.currentLSN, PG_LSN_MAXLENGTH); - - return true; -} - - -/* - * parsePgMetadata parses the result from a PostgreSQL query fetching - * two columns from pg_stat_replication: sync_state and currentLSN. - */ -static void -parsePgReachedTargetLSN(void *ctx, PGresult *result) -{ - PgReachedTargetLSN *context = (PgReachedTargetLSN *) ctx; - - if (PQnfields(result) != 2) - { - log_error("Query returned %d columns, expected 2", PQnfields(result)); - context->parsedOk = false; - return; - } - - if (PQntuples(result) == 0) - { - log_debug("parsePgReachedTargetLSN: query returned no rows"); - context->parsedOk = false; - context->noRows = true; - return; - } - if (PQntuples(result) != 1) - { - log_error("Query returned %d rows, expected 1", PQntuples(result)); - context->parsedOk = false; - return; - } - - context->hasReachedLSN = strcmp(PQgetvalue(result, 0, 0), "t") == 0; - - if (!PQgetisnull(result, 0, 1)) - { - char *value = PQgetvalue(result, 0, 1); - - strlcpy(context->currentLSN, value, PG_LSN_MAXLENGTH); - } - else - { - context->currentLSN[0] = '\0'; - } - - context->parsedOk = true; -} - - -typedef struct IdentifySystemResult -{ - char sqlstate[6]; - bool parsedOk; - IdentifySystem *system; -} IdentifySystemResult; - - -typedef struct TimelineHistoryResult -{ - char sqlstate[6]; - bool parsedOk; - char filename[MAXPGPATH]; - char content[BUFSIZE * BUFSIZE]; /* 1MB should get us quite very far */ -} TimelineHistoryResult; - - -/* - * pgsql_identify_system connects to the given pgsql client and issue the - * replication command IDENTIFY_SYSTEM. The pgsql connection string should - * contain the 'replication=1' parameter. - */ -bool -pgsql_identify_system(PGSQL *pgsql, IdentifySystem *system) -{ - PGconn *connection = pgsql_open_connection(pgsql); - if (connection == NULL) - { - /* error message was logged in pgsql_open_connection */ - return false; - } - - /* extended query protocol not supported in a replication connection */ - PGresult *result = PQexec(connection, "IDENTIFY_SYSTEM"); - - if (!is_response_ok(result)) - { - log_error("Failed to IDENTIFY_SYSTEM: %s", PQerrorMessage(connection)); - PQclear(result); - clear_results(pgsql); - - PQfinish(connection); - - return false; - } - - IdentifySystemResult isContext = { { 0 }, false, system }; - - (void) parseIdentifySystemResult((void *) &isContext, result); - - PQclear(result); - clear_results(pgsql); - - log_debug("IDENTIFY_SYSTEM: timeline %d, xlogpos %s, systemid %" PRIu64, - system->timeline, - system->xlogpos, - system->identifier); - - if (!isContext.parsedOk) - { - log_error("Failed to get result from IDENTIFY_SYSTEM"); - PQfinish(connection); - return false; - } - - /* while at it, we also run the TIMELINE_HISTORY command */ - if (system->timeline > 1) - { - TimelineHistoryResult hContext = { 0 }; - - char sql[BUFSIZE] = { 0 }; - sformat(sql, sizeof(sql), "TIMELINE_HISTORY %d", system->timeline); - - result = PQexec(connection, sql); - - if (!is_response_ok(result)) - { - log_error("Failed to request TIMELINE_HISTORY: %s", - PQerrorMessage(connection)); - PQclear(result); - clear_results(pgsql); - - PQfinish(connection); - - return false; - } - - (void) parseTimelineHistoryResult((void *) &hContext, result); - - PQclear(result); - clear_results(pgsql); - - if (!hContext.parsedOk) - { - log_error("Failed to get result from TIMELINE_HISTORY"); - PQfinish(connection); - return false; - } - - if (!parseTimeLineHistory(hContext.filename, hContext.content, system)) - { - /* errors have already been logged */ - PQfinish(connection); - return false; - } - - TimeLineHistoryEntry *current = - &(system->timelines.history[system->timelines.count - 1]); - - log_debug("TIMELINE_HISTORY: \"%s\", timeline %d started at %X/%X", - hContext.filename, - current->tli, - (uint32_t) (current->begin >> 32), - (uint32_t) current->begin); - } - - /* now we're done with running SQL queries */ - PQfinish(connection); - - return true; -} - - -/* - * parsePgMetadata parses the result from a PostgreSQL query fetching - * two columns from pg_stat_replication: sync_state and currentLSN. - */ -static void -parseIdentifySystemResult(void *ctx, PGresult *result) -{ - IdentifySystemResult *context = (IdentifySystemResult *) ctx; - - if (PQnfields(result) != 4) - { - log_error("Query returned %d columns, expected 4", PQnfields(result)); - context->parsedOk = false; - return; - } - - if (PQntuples(result) == 0) - { - log_debug("parseIdentifySystem: query returned no rows"); - context->parsedOk = false; - return; - } - if (PQntuples(result) != 1) - { - log_error("Query returned %d rows, expected 1", PQntuples(result)); - context->parsedOk = false; - return; - } - - /* systemid (text) */ - char *value = PQgetvalue(result, 0, 0); - if (!stringToUInt64(value, &(context->system->identifier))) - { - log_error("Failed to parse system_identifier \"%s\"", value); - context->parsedOk = false; - return; - } - - /* timeline (int4) */ - value = PQgetvalue(result, 0, 1); - if (!stringToUInt32(value, &(context->system->timeline))) - { - log_error("Failed to parse timeline \"%s\"", value); - context->parsedOk = false; - return; - } - - /* xlogpos (text) */ - value = PQgetvalue(result, 0, 2); - strlcpy(context->system->xlogpos, value, PG_LSN_MAXLENGTH); - - /* dbname (text) Database connected to or null */ - if (!PQgetisnull(result, 0, 3)) - { - value = PQgetvalue(result, 0, 3); - strlcpy(context->system->dbname, value, NAMEDATALEN); - } - - context->parsedOk = true; -} - - -/* - * parseTimelineHistory parses the result of the TIMELINE_HISTORY replication - * command. - */ -static void -parseTimelineHistoryResult(void *ctx, PGresult *result) -{ - TimelineHistoryResult *context = (TimelineHistoryResult *) ctx; - - if (PQnfields(result) != 2) - { - log_error("Query returned %d columns, expected 2", PQnfields(result)); - context->parsedOk = false; - return; - } - - if (PQntuples(result) == 0) - { - log_debug("parseTimelineHistory: query returned no rows"); - context->parsedOk = false; - return; - } - - if (PQntuples(result) != 1) - { - log_error("Query returned %d rows, expected 1", PQntuples(result)); - context->parsedOk = false; - return; - } - - /* filename (text) */ - char *value = PQgetvalue(result, 0, 0); - strlcpy(context->filename, value, sizeof(context->filename)); - - /* content (bytea) */ - value = PQgetvalue(result, 0, 1); - - if (strlen(value) >= sizeof(context->content)) - { - log_error("Received a timeline history file of %lu bytes, " - "pg_autoctl is limited to files of up to %lu bytes.", - (unsigned long) strlen(value), - (unsigned long) sizeof(context->content)); - context->parsedOk = false; - } - strlcpy(context->content, value, sizeof(context->content)); - - context->parsedOk = true; -} - - -/* - * parseTimeLineHistory parses the content of a timeline history file. - */ -bool -parseTimeLineHistory(const char *filename, const char *content, - IdentifySystem *system) -{ - int lineCount = countLines((char *) content); - char **historyLines = (char **) calloc(lineCount, sizeof(char *)); - int lineNumber = 0; - - if (historyLines == NULL) - { - log_error(ALLOCATION_FAILED_ERROR); - return false; - } - - splitLines((char *) content, historyLines, lineCount); - - uint64_t prevend = InvalidXLogRecPtr; - - system->timelines.count = 0; - - TimeLineHistoryEntry *entry = - &(system->timelines.history[system->timelines.count]); - - for (lineNumber = 0; lineNumber < lineCount; lineNumber++) - { - char *ptr = historyLines[lineNumber]; - - /* skip leading whitespace and check for # comment */ - for (; *ptr; ptr++) - { - if (!isspace((unsigned char) *ptr)) - { - break; - } - } - - if (*ptr == '\0' || *ptr == '#') - { - continue; - } - - log_trace("parseTimeLineHistory line %d is \"%s\"", - lineNumber, - historyLines[lineNumber]); - - char *tabptr = strchr(historyLines[lineNumber], '\t'); - - if (tabptr == NULL) - { - log_error("Failed to parse history file line %d: \"%s\"", - lineNumber, ptr); - free(historyLines); - return false; - } - - *tabptr = '\0'; - - if (!stringToUInt(historyLines[lineNumber], &(entry->tli))) - { - log_error("Failed to parse history timeline \"%s\"", tabptr); - free(historyLines); - return false; - } - - char *lsn = tabptr + 1; - - for (char *lsnend = lsn; *lsnend; lsnend++) - { - if (!(isxdigit((unsigned char) *lsnend) || *lsnend == '/')) - { - *lsnend = '\0'; - break; - } - } - - if (!parseLSN(lsn, &(entry->end))) - { - log_error("Failed to parse history timeline %d LSN \"%s\"", - entry->tli, lsn); - free(historyLines); - return false; - } - - entry->begin = prevend; - prevend = entry->end; - - log_trace("parseTimeLineHistory[%d]: tli %d [%X/%X %X/%X]", - system->timelines.count, - entry->tli, - (uint32) (entry->begin >> 32), - (uint32) entry->begin, - (uint32) (entry->end >> 32), - (uint32) entry->end); - - entry = &(system->timelines.history[++system->timelines.count]); - } - - free(historyLines); - - /* - * Create one more entry for the "tip" of the timeline, which has no entry - * in the history file. - */ - entry->tli = system->timeline; - entry->begin = prevend; - entry->end = InvalidXLogRecPtr; - - log_trace("parseTimeLineHistory[%d]: tli %d [%X/%X %X/%X]", - system->timelines.count, - entry->tli, - (uint32) (entry->begin >> 32), - (uint32) entry->begin, - (uint32) (entry->end >> 32), - (uint32) entry->end); - - /* fix the off-by-one so that the count is a count, not an index */ - ++system->timelines.count; - - return true; -} - - -/* - * LISTEN/NOTIFY support. - * - * First, send a LISTEN command. - */ -bool -pgsql_listen(PGSQL *pgsql, char *channels[]) -{ - PGresult *result = NULL; - char sql[BUFSIZE]; - - /* - * mark the connection as multi statement since it is going to be used by - * for processing notifications - */ - pgsql->connectionStatementType = PGSQL_CONNECTION_MULTI_STATEMENT; - - /* open a connection upfront since it is needed by PQescape functions */ - PGconn *connection = pgsql_open_connection(pgsql); - if (connection == NULL) - { - /* error message was logged in pgsql_open_connection */ - return false; - } - - for (int i = 0; channels[i]; i++) - { - char *channel = - PQescapeIdentifier(connection, channels[i], strlen(channels[i])); - - if (channel == NULL) - { - log_error("Failed to LISTEN \"%s\": %s", - channels[i], PQerrorMessage(connection)); - pgsql_finish(pgsql); - return false; - } - - sformat(sql, BUFSIZE, "LISTEN %s", channel); - - PQfreemem(channel); - - result = PQexec(connection, sql); - - if (!is_response_ok(result)) - { - log_error("Failed to LISTEN \"%s\": %s", - channels[i], PQerrorMessage(connection)); - PQclear(result); - clear_results(pgsql); - - return false; - } - - PQclear(result); - clear_results(pgsql); - } - - return true; -} - - -/* - * Preapre a multi statement connection which can later be used in wait for - * notification functions. - * - * Contrarry to pgsql_listen, this function, only prepares the connection and it - * is the user's responsibility to define which channels to listen to. - */ -bool -pgsql_prepare_to_wait(PGSQL *pgsql) -{ - /* - * mark the connection as multi statement since it is going to be used by - * for processing notifications - */ - pgsql->connectionStatementType = PGSQL_CONNECTION_MULTI_STATEMENT; - - /* open a connection upfront since it is needed by PQescape functions */ - PGconn *connection = pgsql_open_connection(pgsql); - if (connection == NULL) - { - /* error message was logged in pgsql_open_connection */ - return false; - } - - return true; -} - - -/* - * pgsql_alter_extension_update_to executes ALTER EXTENSION ... UPDATE TO ... - */ -bool -pgsql_alter_extension_update_to(PGSQL *pgsql, - const char *extname, const char *version) -{ - char command[BUFSIZE]; - char *escapedIdentifier, *escapedVersion; - - /* open a connection upfront since it is needed by PQescape functions */ - PGconn *connection = pgsql_open_connection(pgsql); - if (connection == NULL) - { - /* error message was logged in pgsql_open_connection */ - return false; - } - - /* escape the extname */ - escapedIdentifier = PQescapeIdentifier(connection, extname, strlen(extname)); - if (escapedIdentifier == NULL) - { - log_error("Failed to update extension \"%s\": %s", extname, - PQerrorMessage(connection)); - pgsql_finish(pgsql); - return false; - } - - /* escape the version */ - escapedVersion = PQescapeIdentifier(connection, version, strlen(version)); - if (escapedIdentifier == NULL) - { - log_error("Failed to update extension \"%s\" to version \"%s\": %s", - extname, version, - PQerrorMessage(connection)); - pgsql_finish(pgsql); - return false; - } - - /* now build the SQL command */ - int n = sformat(command, BUFSIZE, "ALTER EXTENSION %s UPDATE TO %s", - escapedIdentifier, escapedVersion); - - if (n >= BUFSIZE) - { - log_error("BUG: pg_autoctl only supports SQL string up to %d bytes, " - "a SQL string of %d bytes is needed to " - "update the \"%s\" extension.", - BUFSIZE, n, extname); - } - - PQfreemem(escapedIdentifier); - PQfreemem(escapedVersion); - - log_debug("Running command on Postgres: %s;", command); - - PGresult *result = PQexec(connection, command); - - if (!is_response_ok(result)) - { - char *sqlstate = PQresultErrorField(result, PG_DIAG_SQLSTATE); - - log_error("Error %s while running Postgres query: %s:", - sqlstate, command); - - char *message = PQerrorMessage(connection); - char *errorLines[BUFSIZE]; - int lineCount = splitLines(message, errorLines, BUFSIZE); - int lineNumber = 0; - - for (lineNumber = 0; lineNumber < lineCount; lineNumber++) - { - log_error("%s", errorLines[lineNumber]); - } - - PQclear(result); - clear_results(pgsql); - pgsql_finish(pgsql); - return false; - } - - PQclear(result); - clear_results(pgsql); - - return true; -} diff --git a/src/bin/pg_autoctl/pgsql.h b/src/bin/pg_autoctl/pgsql.h deleted file mode 100644 index 792ce7d57..000000000 --- a/src/bin/pg_autoctl/pgsql.h +++ /dev/null @@ -1,400 +0,0 @@ -/* - * src/bin/pg_autoctl/pgsql.h - * Functions for interacting with a postgres server - * - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the PostgreSQL License. - * - */ - -#ifndef PGSQL_H -#define PGSQL_H - - -#include -#include - -#include "postgres.h" -#include "libpq-fe.h" -#include "portability/instr_time.h" - -#if PG_MAJORVERSION_NUM >= 15 -#include "common/pg_prng.h" -#endif - -#include "defaults.h" -#include "pgsetup.h" -#include "state.h" - - -/* - * OID values from PostgreSQL src/include/catalog/pg_type.h - */ -#define BOOLOID 16 -#define NAMEOID 19 -#define INT4OID 23 -#define INT8OID 20 -#define TEXTOID 25 -#define LSNOID 3220 - -/* - * Maximum connection info length as used in walreceiver.h - */ -#define MAXCONNINFO 1024 - - -/* - * pg_stat_replication.sync_state is one if: - * sync, async, quorum, potential - */ -#define PGSR_SYNC_STATE_MAXLENGTH 10 - -/* - * We receive a list of "other nodes" from the monitor, and we store that list - * in local memory. We pre-allocate the memory storage, and limit how many node - * addresses we can handle because of the pre-allocation strategy. - */ -#define NODE_ARRAY_MAX_COUNT 128 - - -/* abstract representation of a Postgres server that we can connect to */ -typedef enum -{ - PGSQL_CONN_LOCAL = 0, - PGSQL_CONN_MONITOR, - PGSQL_CONN_COORDINATOR, - PGSQL_CONN_UPSTREAM, - PGSQL_CONN_APP -} ConnectionType; - - -/* - * Retry policy to follow when we fail to connect to a Postgres URI. - * - * In almost all the code base the retry mechanism is implemented in the main - * loop so we want to fail fast and let the main loop handle the connection - * retry and the different network timeouts that we have, including the network - * partition detection timeout. - * - * In the initialisation code path though, pg_autoctl might be launched from - * provisioning script on a set of nodes in parallel, and in that case we need - * to secure a connection and implement a retry policy at the point in the code - * where we open a connection, so that it's transparent to the caller. - * - * When we do retry connecting, we implement an Exponential Backoff with - * Decorrelated Jitter algorithm as proven useful in the following article: - * - * https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/ - */ -typedef struct ConnectionRetryPolicy -{ - int maxT; /* maximum time spent retrying (seconds) */ - int maxR; /* maximum number of retries, might be zero */ - int maxSleepTime; /* in millisecond, used to cap sleepTime */ - int baseSleepTime; /* in millisecond, base time to sleep for */ - int sleepTime; /* in millisecond, time waited for last round */ - - instr_time startTime; /* time of the first attempt */ - instr_time connectTime; /* time of successful connection */ - int attempts; /* how many attempts have been made so far */ - -#if PG_MAJORVERSION_NUM >= 15 - pg_prng_state prng_state; -#endif -} ConnectionRetryPolicy; - -/* - * Denote if the connetion is going to be used for one, or multiple statements. - * This is used by psql_* functions to know if a connection is to be closed - * after successful completion, or if the the connection is to be maintained - * open for further queries. - * - * A common use case for maintaining a connection open, is while wishing to open - * and maintain a transaction block. Another, is while listening for events. - */ -typedef enum -{ - PGSQL_CONNECTION_SINGLE_STATEMENT = 0, - PGSQL_CONNECTION_MULTI_STATEMENT -} ConnectionStatementType; - -/* - * Allow higher level code to distinguish between failure to connect to the - * target Postgres service and failure to run a query or obtain the expected - * result. To that end we expose PQstatus() of the connection. - * - * We don't use the same enum values as in libpq because we want to have the - * unknown value when we didn't try to connect yet. - */ -typedef enum -{ - PG_CONNECTION_UNKNOWN = 0, - PG_CONNECTION_OK, - PG_CONNECTION_BAD -} PGConnStatus; - -/* notification processing */ -typedef bool (*ProcessNotificationFunction)(int notificationGroupId, - int64_t notificationNodeId, - char *channel, char *payload); - -typedef struct PGSQL -{ - ConnectionType connectionType; - ConnectionStatementType connectionStatementType; - char connectionString[MAXCONNINFO]; - PGconn *connection; - ConnectionRetryPolicy retryPolicy; - PGConnStatus status; - - ProcessNotificationFunction notificationProcessFunction; - int notificationGroupId; - int64_t notificationNodeId; - bool notificationReceived; -} PGSQL; - - -/* PostgreSQL ("Grand Unified Configuration") setting */ -typedef struct GUC -{ - char *name; - char *value; -} GUC; - -/* network address of a node in an HA group */ -typedef struct NodeAddress -{ - int64_t nodeId; - char name[_POSIX_HOST_NAME_MAX]; - char host[_POSIX_HOST_NAME_MAX]; - int port; - int tli; - char lsn[PG_LSN_MAXLENGTH]; - bool isPrimary; -} NodeAddress; - -typedef struct NodeAddressArray -{ - int count; - NodeAddress nodes[NODE_ARRAY_MAX_COUNT]; -} NodeAddressArray; - - -/* - * TimeLineHistoryEntry is taken from Postgres definitions and adapted to - * client-size code where we don't have all the necessary infrastruture. In - * particular we don't define a XLogRecPtr data type nor do we define a - * TimeLineID data type. - * - * Zero is used indicate an invalid pointer. Bootstrap skips the first possible - * WAL segment, initializing the first WAL page at WAL segment size, so no XLOG - * record can begin at zero. - */ -#define InvalidXLogRecPtr 0 -#define XLogRecPtrIsInvalid(r) ((r) == InvalidXLogRecPtr) - -#define PG_AUTOCTL_MAX_TIMELINES 1024 - -typedef struct TimeLineHistoryEntry -{ - uint32_t tli; - uint64_t begin; /* inclusive */ - uint64_t end; /* exclusive, InvalidXLogRecPtr means infinity */ -} TimeLineHistoryEntry; - - -typedef struct TimeLineHistory -{ - int count; - TimeLineHistoryEntry history[PG_AUTOCTL_MAX_TIMELINES]; -} TimeLineHistory; - - -/* - * The IdentifySystem contains information that is parsed from the - * IDENTIFY_SYSTEM replication command, and then the TIMELINE_HISTORY result. - */ -typedef struct IdentifySystem -{ - uint64_t identifier; - uint32_t timeline; - char xlogpos[PG_LSN_MAXLENGTH]; - char dbname[NAMEDATALEN]; - TimeLineHistory timelines; -} IdentifySystem; - - -/* - * The replicationSource structure is used to pass the bits of a connection - * string to the primary node around in several function calls. All the - * information stored in there must fit in a connection string, so MAXCONNINFO - * is a good proxy for their maximum size. - */ -typedef struct ReplicationSource -{ - NodeAddress primaryNode; - char userName[NAMEDATALEN]; - char slotName[MAXCONNINFO]; - char password[MAXCONNINFO]; - char maximumBackupRate[MAXIMUM_BACKUP_RATE_LEN]; - char backupDir[MAXCONNINFO]; - char applicationName[MAXCONNINFO]; - char targetLSN[PG_LSN_MAXLENGTH]; - char targetAction[NAMEDATALEN]; - char targetTimeline[NAMEDATALEN]; - SSLOptions sslOptions; - IdentifySystem system; -} ReplicationSource; - - -/* - * Arrange a generic way to parse PostgreSQL result from a query. Most of the - * queries we need here return a single row of a single column, so that's what - * the default context and parsing allows for. - */ - -/* callback for parsing query results */ -typedef void (ParsePostgresResultCB)(void *context, PGresult *result); - -typedef enum -{ - PGSQL_RESULT_BOOL = 1, - PGSQL_RESULT_INT, - PGSQL_RESULT_BIGINT, - PGSQL_RESULT_STRING -} QueryResultType; - -/* - * As a way to communicate the SQL STATE when an error occurs, every - * pgsql_execute_with_params context structure must have the same first field, - * an array of 5 characters (plus '\0' at the end). - */ -#define SQLSTATE_LENGTH 6 - -#define STR_ERRCODE_CLASS_CONNECTION_EXCEPTION "08" - -typedef struct AbstractResultContext -{ - char sqlstate[SQLSTATE_LENGTH]; -} AbstractResultContext; - -/* data structure for keeping a single-value query result */ -typedef struct SingleValueResultContext -{ - char sqlstate[SQLSTATE_LENGTH]; - QueryResultType resultType; - bool parsedOk; - int ntuples; - bool boolVal; - int intVal; - uint64_t bigint; - char *strVal; -} SingleValueResultContext; - - -#define CHECK__SETTINGS_SQL \ - "select bool_and(ok) " \ - "from (" \ - "select current_setting('max_wal_senders')::int >= 12" \ - " union all " \ - "select current_setting('max_replication_slots')::int >= 12" \ - " union all " \ - "select current_setting('wal_level') in ('replica', 'logical')" \ - " union all " \ - "select current_setting('wal_log_hints') = 'on'" - -#define CHECK_POSTGRESQL_NODE_SETTINGS_SQL \ - CHECK__SETTINGS_SQL \ - ") as t(ok) " - -#define CHECK_CITUS_NODE_SETTINGS_SQL \ - CHECK__SETTINGS_SQL \ - " union all " \ - "select lib = 'citus' " \ - "from unnest(string_to_array(" \ - "current_setting('shared_preload_libraries'), ',') " \ - " || array['not citus']) " \ - "with ordinality ast(lib, n) where n = 1" \ - ") as t(ok) " - -bool pgsql_init(PGSQL *pgsql, char *url, ConnectionType connectionType); - -void pgsql_set_retry_policy(ConnectionRetryPolicy *retryPolicy, - int maxT, - int maxR, - int maxSleepTime, - int baseSleepTime); -void pgsql_set_main_loop_retry_policy(ConnectionRetryPolicy *retryPolicy); -void pgsql_set_init_retry_policy(ConnectionRetryPolicy *retryPolicy); -void pgsql_set_interactive_retry_policy(ConnectionRetryPolicy *retryPolicy); -void pgsql_set_monitor_interactive_retry_policy(ConnectionRetryPolicy *retryPolicy); -int pgsql_compute_connection_retry_sleep_time(ConnectionRetryPolicy *retryPolicy); -bool pgsql_retry_policy_expired(ConnectionRetryPolicy *retryPolicy); - -void pgsql_finish(PGSQL *pgsql); -void parseSingleValueResult(void *ctx, PGresult *result); -void fetchedRows(void *ctx, PGresult *result); -bool pgsql_begin(PGSQL *pgsql); -bool pgsql_commit(PGSQL *pgsql); -bool pgsql_rollback(PGSQL *pgsql); -bool pgsql_execute(PGSQL *pgsql, const char *sql); -bool pgsql_execute_with_params(PGSQL *pgsql, const char *sql, int paramCount, - const Oid *paramTypes, const char **paramValues, - void *parseContext, ParsePostgresResultCB *parseFun); -bool pgsql_check_postgresql_settings(PGSQL *pgsql, bool isCitusInstanceKind, - bool *settings_are_ok); -bool pgsql_check_monitor_settings(PGSQL *pgsql, bool *settings_are_ok); -bool pgsql_is_in_recovery(PGSQL *pgsql, bool *is_in_recovery); -bool pgsql_reload_conf(PGSQL *pgsql); -bool pgsql_replication_slot_exists(PGSQL *pgsql, const char *slotName, - bool *slotExists); -bool pgsql_create_replication_slot(PGSQL *pgsql, const char *slotName); -bool pgsql_drop_replication_slot(PGSQL *pgsql, const char *slotName); -bool postgres_sprintf_replicationSlotName(int64_t nodeId, char *slotName, int size); -bool pgsql_set_synchronous_standby_names(PGSQL *pgsql, - char *synchronous_standby_names); -bool pgsql_replication_slot_create_and_drop(PGSQL *pgsql, - NodeAddressArray *nodeArray); -bool pgsql_replication_slot_maintain(PGSQL *pgsql, NodeAddressArray *nodeArray); -bool pgsql_disable_synchronous_replication(PGSQL *pgsql); -bool pgsql_set_default_transaction_mode_read_only(PGSQL *pgsql); -bool pgsql_set_default_transaction_mode_read_write(PGSQL *pgsql); -bool pgsql_checkpoint(PGSQL *pgsql); -bool pgsql_get_hba_file_path(PGSQL *pgsql, char *hbaFilePath, int maxPathLength); -bool pgsql_create_database(PGSQL *pgsql, const char *dbname, const char *owner); -bool pgsql_create_extension(PGSQL *pgsql, const char *name); -bool pgsql_create_user(PGSQL *pgsql, const char *userName, const char *password, - bool login, bool superuser, bool replication, - int connlimit); -bool pgsql_has_replica(PGSQL *pgsql, char *userName, bool *hasReplica); -bool hostname_from_uri(const char *pguri, - char *hostname, int maxHostLength, int *port); -bool validate_connection_string(const char *connectionString); -bool pgsql_reset_primary_conninfo(PGSQL *pgsql); - -bool pgsql_get_postgres_metadata(PGSQL *pgsql, - bool *pg_is_in_recovery, - char *pgsrSyncState, char *currentLSN, - PostgresControlData *control); - -bool pgsql_one_slot_has_reached_target_lsn(PGSQL *pgsql, - char *targetLSN, - char *currentLSN, - bool *hasReachedLSN); -bool pgsql_has_reached_target_lsn(PGSQL *pgsql, char *targetLSN, - char *currentLSN, bool *hasReachedLSN); -bool pgsql_identify_system(PGSQL *pgsql, IdentifySystem *system); -bool pgsql_listen(PGSQL *pgsql, char *channels[]); -bool pgsql_prepare_to_wait(PGSQL *pgsql); - -bool pgsql_alter_extension_update_to(PGSQL *pgsql, - const char *extname, const char *version); - -bool parseTimeLineHistory(const char *filename, const char *content, - IdentifySystem *system); - -bool pgsql_alter_role_password(PGSQL *pgsql, const char *roleName, - const char *password); - - -#endif /* PGSQL_H */ diff --git a/src/bin/pg_autoctl/pgtuning.c b/src/bin/pg_autoctl/pgtuning.c deleted file mode 100644 index 50b88eecb..000000000 --- a/src/bin/pg_autoctl/pgtuning.c +++ /dev/null @@ -1,390 +0,0 @@ -/* - * src/bin/pg_autoctl/pgtuning.c - * Adjust some very basic Postgres tuning to the system properties. - * - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the PostgreSQL License. - * - */ - -#include "postgres_fe.h" -#include "pqexpbuffer.h" - -#include "config.h" -#include "env_utils.h" -#include "file_utils.h" -#include "log.h" -#include "pgctl.h" -#include "pgtuning.h" -#include "system_utils.h" - -/* - * In most cases we are going to initdb a Postgres instance for our users, we - * might as well introduce some naive Postgres tuning. In the static array are - * selected Postgres default values and static values we always set. - * - * Dynamic code is then used on the target systems to compute better values - * dynamically for some parameters: work_mem, maintenance_work_mem, - * effective_cache_size, autovacuum_max_workers. - */ -GUC postgres_tuning[] = { - { "track_functions", "pl" }, - { "shared_buffers", "'128 MB'" }, - { "work_mem", "'4 MB'" }, - { "maintenance_work_mem", "'64MB'" }, - { "effective_cache_size", "'4 GB'" }, - { "autovacuum_max_workers", "3" }, - { "autovacuum_vacuum_scale_factor", "0.08" }, - { "autovacuum_analyze_scale_factor", "0.02" }, - { NULL, NULL } -}; - - -typedef struct DynamicTuning -{ - int autovacuum_max_workers; - uint64_t shared_buffers; - uint64_t work_mem; - uint64_t maintenance_work_mem; - uint64_t effective_cache_size; -} DynamicTuning; - - -static bool pgtuning_compute_mem_settings(SystemInfo *sysInfo, - DynamicTuning *tuning); - -void pgtuning_log_settings(DynamicTuning *tuning, int logLevel); - -static int pgtuning_compute_max_workers(SystemInfo *sysInfo); - -static bool pgtuning_edit_guc_settings(GUC *settings, DynamicTuning *tuning, - char *config, size_t size); - - -/* - * pgtuning_prepare_guc_settings probes the system information (nCPU and total - * RAM) and computes some better defaults for Postgres. - */ -bool -pgtuning_prepare_guc_settings(GUC *settings, char *config, size_t size) -{ - SystemInfo sysInfo = { 0 }; - DynamicTuning tuning = { 0 }; - char totalram[BUFSIZE] = { 0 }; - - if (!get_system_info(&sysInfo)) - { - /* errors have already been logged */ - return false; - } - - (void) pretty_print_bytes(totalram, sizeof(totalram), sysInfo.totalram); - - log_debug("Detected %d CPUs and %s total RAM on this server", - sysInfo.ncpu, - totalram); - - /* - * Disable Postgres tuning when running the unit test suite: we install our - * default set of values rather than computing better values for the - * current environment. - */ - if (!(env_exists(PG_AUTOCTL_DEBUG) && env_exists("PG_REGRESS_SOCK_DIR"))) - { - tuning.autovacuum_max_workers = pgtuning_compute_max_workers(&sysInfo); - - if (!pgtuning_compute_mem_settings(&sysInfo, &tuning)) - { - log_error("Failed to compute memory settings, using defaults"); - return false; - } - - (void) pgtuning_log_settings(&tuning, LOG_DEBUG); - } - - return pgtuning_edit_guc_settings(settings, &tuning, config, size); -} - - -/* - * pgtuning_compute_max_workers returns how many autovacuum max workers we can - * setup on the local system, depending on its number of CPUs. - * - * We could certainly cook a simple enough maths expression to compute the - * numbers assigned in this range based "grid" here, but that would be much - * harder to maintain and change our mind about, and not as easy to grasp on a - * quick reading. - */ -static int -pgtuning_compute_max_workers(SystemInfo *sysInfo) -{ - /* use the default up to 16 cores (HT included) */ - if (sysInfo->ncpu < 16) - { - return 3; - } - else if (sysInfo->ncpu < 24) - { - return 4; - } - else if (sysInfo->ncpu < 32) - { - return 6; - } - else if (sysInfo->ncpu < 48) - { - return 8; - } - else if (sysInfo->ncpu < 64) - { - return 12; - } - else - { - return 16; - } -} - - -/* - * pgtuning_compute_work_mem computes how much work mem to use on this system. - * - * Inspiration has been taken from http://pgconfigurator.cybertec.at - * - * Rather than trying to devise a good maths expression to compute values, we - * implement our decision making with a range based approach. Some values are - * still computed with an expression (shared_buffers is set to 25% of the total - * RAM up to 256 GB of RAM, for instance). - */ -static bool -pgtuning_compute_mem_settings(SystemInfo *sysInfo, DynamicTuning *tuning) -{ - uint64_t oneGB = ((uint64_t) 1) << 30; - - /* - * <= 8 GB of RAM - */ - if (sysInfo->totalram <= (8 * oneGB)) - { - tuning->shared_buffers = sysInfo->totalram / 4; - tuning->work_mem = 16 * 1 << 20; /* 16 MB */ - tuning->maintenance_work_mem = 256 * 1 << 20; /* 256 MB */ - } - - /* - * > 8 GB up to 64 GB of RAM - */ - else if (sysInfo->totalram <= (64 * oneGB)) - { - tuning->shared_buffers = sysInfo->totalram / 4; - tuning->work_mem = 24 * 1 << 20; /* 24 MB */ - tuning->maintenance_work_mem = 512 * 1 << 20; /* 512 MB */ - } - - /* - * > 64 GB up to 256 GB of RAM - */ - else if (sysInfo->totalram <= (256 * oneGB)) - { - tuning->shared_buffers = 16 * oneGB; /* 16 GB */ - tuning->work_mem = 32 * 1 << 20; /* 32 MB */ - tuning->maintenance_work_mem = oneGB; /* 1 GB */ - } - - /* - * > 256 GB of RAM - */ - else - { - tuning->shared_buffers = 32 * oneGB; /* 32 GB */ - tuning->work_mem = 64 * 1 << 20; /* 64 MB */ - tuning->maintenance_work_mem = 2 * oneGB; /* 2 GB */ - } - - /* - * What's not in shared buffers is expected to be mostly file system cache, - * and then again effective_cache_size is a hint and does not need to be - * the exact value as shown by the free(1) command. - */ - tuning->effective_cache_size = sysInfo->totalram - tuning->shared_buffers; - - return true; -} - - -/* - * pgtuning_log_mem_settings logs the memory settings we computed. - */ -void -pgtuning_log_settings(DynamicTuning *tuning, int logLevel) -{ - char buf[BUFSIZE] = { 0 }; - - log_level(logLevel, - "Setting autovacuum_max_workers to %d", - tuning->autovacuum_max_workers); - - (void) pretty_print_bytes(buf, sizeof(buf), tuning->shared_buffers); - log_level(logLevel, "Setting shared_buffers to %s", buf); - - (void) pretty_print_bytes(buf, sizeof(buf), tuning->work_mem); - log_level(logLevel, "Setting work_mem to %s", buf); - - (void) pretty_print_bytes(buf, sizeof(buf), - tuning->maintenance_work_mem); - log_level(logLevel, "Setting maintenance_work_mem to %s", buf); - - (void) pretty_print_bytes(buf, sizeof(buf), - tuning->effective_cache_size); - log_level(logLevel, "Setting effective_cache_size to %s", buf); -} - - -/* - * pgtuning_edit_guc_settings prepares a Postgres configuration file snippet - * from the given GUC settings and the dynamic tuning adjusted to the system - * and place the resulting snippet in the pre-allocated string buffer config of - * given size. - */ -#define streq(x, y) ((x != NULL) && (y != NULL) && (strcmp(x, y) == 0)) - -static bool -pgtuning_edit_guc_settings(GUC *settings, DynamicTuning *tuning, - char *config, size_t size) -{ - PQExpBuffer contents = createPQExpBuffer(); - int settingIndex = 0; - - if (contents == NULL) - { - log_error("Failed to allocate memory"); - return false; - } - - appendPQExpBuffer(contents, - "# basic tuning computed by pg_auto_failover\n"); - - /* replace placeholder values with dynamic tuned values */ - for (settingIndex = 0; settings[settingIndex].name != NULL; settingIndex++) - { - GUC *setting = &settings[settingIndex]; - - if (streq(setting->name, "autovacuum_max_workers")) - { - if (tuning->autovacuum_max_workers > 0) - { - appendPQExpBuffer(contents, "%s = %d\n", - setting->name, - tuning->autovacuum_max_workers); - } - else - { - appendPQExpBuffer(contents, "%s = %s\n", - setting->name, - setting->value); - } - } - else if (streq(setting->name, "shared_buffers")) - { - if (tuning->shared_buffers > 0) - { - char pretty[BUFSIZE] = { 0 }; - - (void) pretty_print_bytes(pretty, sizeof(pretty), - tuning->shared_buffers); - - appendPQExpBuffer(contents, "%s = '%s'\n", - setting->name, pretty); - } - else - { - appendPQExpBuffer(contents, "%s = %s\n", - setting->name, setting->value); - } - } - else if (streq(setting->name, "work_mem")) - { - if (tuning->work_mem > 0) - { - char pretty[BUFSIZE] = { 0 }; - - (void) pretty_print_bytes(pretty, sizeof(pretty), - tuning->work_mem); - - appendPQExpBuffer(contents, "%s = '%s'\n", - setting->name, pretty); - } - else - { - appendPQExpBuffer(contents, "%s = %s\n", - setting->name, setting->value); - } - } - else if (streq(setting->name, "maintenance_work_mem")) - { - if (tuning->maintenance_work_mem > 0) - { - char pretty[BUFSIZE] = { 0 }; - - (void) pretty_print_bytes(pretty, sizeof(pretty), - tuning->maintenance_work_mem); - - appendPQExpBuffer(contents, "%s = '%s'\n", - setting->name, pretty); - } - else - { - appendPQExpBuffer(contents, "%s = %s\n", - setting->name, setting->value); - } - } - else if (streq(setting->name, "effective_cache_size")) - { - if (tuning->effective_cache_size > 0) - { - char pretty[BUFSIZE] = { 0 }; - - (void) pretty_print_bytes(pretty, sizeof(pretty), - tuning->effective_cache_size); - - appendPQExpBuffer(contents, "%s = '%s'\n", - setting->name, pretty); - } - else - { - appendPQExpBuffer(contents, "%s = %s\n", - setting->name, setting->value); - } - } - else - { - appendPQExpBuffer(contents, "%s = %s\n", - setting->name, setting->value); - } - } - - /* memory allocation could have failed while building string */ - if (PQExpBufferBroken(contents)) - { - log_error("Failed to allocate memory"); - destroyPQExpBuffer(contents); - return false; - } - - if (size < contents->len) - { - log_error("Failed to prepare Postgres tuning for the local system, " - "the setup needs %lu bytes and pg_autoctl only support " - "up to %lu bytes", - (unsigned long) contents->len, - (unsigned long) size); - destroyPQExpBuffer(contents); - return false; - } - - strlcpy(config, contents->data, size); - - destroyPQExpBuffer(contents); - - return true; -} diff --git a/src/bin/pg_autoctl/pgtuning.h b/src/bin/pg_autoctl/pgtuning.h deleted file mode 100644 index de4a98044..000000000 --- a/src/bin/pg_autoctl/pgtuning.h +++ /dev/null @@ -1,19 +0,0 @@ -/* - * src/bin/pg_autoctl/pgtuning.h - * Adjust some very basic Postgres tuning to the system properties. - * - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the PostgreSQL License. - * - */ - -#ifndef PGTUNING_H -#define PGTUNING_H - -#include - -extern GUC postgres_tuning[]; - -bool pgtuning_prepare_guc_settings(GUC *settings, char *config, size_t size); - -#endif /* PGTUNING_H */ diff --git a/src/bin/pg_autoctl/pidfile.c b/src/bin/pg_autoctl/pidfile.c deleted file mode 100644 index 931b67ed9..000000000 --- a/src/bin/pg_autoctl/pidfile.c +++ /dev/null @@ -1,568 +0,0 @@ -/* - * src/bin/pg_autoctl/pidfile.c - * Utilities to manage the pg_autoctl pidfile. - * - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the PostgreSQL License. - * - */ - -#include -#include -#include -#include -#include -#include - -#include "postgres_fe.h" -#include "pqexpbuffer.h" - -#include "cli_common.h" -#include "cli_root.h" -#include "defaults.h" -#include "env_utils.h" -#include "fsm.h" -#include "keeper.h" -#include "keeper_config.h" -#include "keeper_pg_init.h" -#include "lock_utils.h" -#include "log.h" -#include "monitor.h" -#include "pgctl.h" -#include "pidfile.h" -#include "state.h" -#include "signals.h" -#include "string_utils.h" - -/* pidfile for this process */ -char service_pidfile[MAXPGPATH] = { 0 }; - -static void remove_service_pidfile_atexit(void); - -/* - * create_pidfile writes our pid in a file. - * - * When running in a background loop, we need a pidFile to add a command line - * tool that send signals to the process. The pidfile has a single line - * containing our PID. - */ -bool -create_pidfile(const char *pidfile, pid_t pid) -{ - PQExpBuffer content = createPQExpBuffer(); - - - log_trace("create_pidfile(%d): \"%s\"", pid, pidfile); - - if (content == NULL) - { - log_fatal("Failed to allocate memory to update our PID file"); - return false; - } - - if (!prepare_pidfile_buffer(content, pid)) - { - /* errors have already been logged */ - destroyPQExpBuffer(content); - return false; - } - - - /* memory allocation could have failed while building string */ - if (PQExpBufferBroken(content)) - { - log_error("Failed to create pidfile \"%s\": out of memory", pidfile); - destroyPQExpBuffer(content); - return false; - } - - bool success = write_file(content->data, content->len, pidfile); - destroyPQExpBuffer(content); - - return success; -} - - -/* - * prepare_pidfile_buffer prepares a PQExpBuffer content with the information - * expected to be found in a pidfile. - */ -bool -prepare_pidfile_buffer(PQExpBuffer content, pid_t pid) -{ - char pgdata[MAXPGPATH] = { 0 }; - - /* we get PGDATA from the environment */ - if (!get_env_pgdata(pgdata)) - { - log_fatal("Failed to get PGDATA to create the PID file"); - return false; - } - - /* - * line # - * 1 supervisor PID - * 2 data directory path - * 3 version number (PG_AUTOCTL_VERSION) - * 4 extension version number (PG_AUTOCTL_EXTENSION_VERSION) - * 5 shared semaphore id (used to serialize log writes) - */ - appendPQExpBuffer(content, "%d\n", pid); - appendPQExpBuffer(content, "%s\n", pgdata); - appendPQExpBuffer(content, "%s\n", PG_AUTOCTL_VERSION); - appendPQExpBuffer(content, "%s\n", PG_AUTOCTL_EXTENSION_VERSION); - appendPQExpBuffer(content, "%d\n", log_semaphore.semId); - - return true; -} - - -/* - * create_pidfile writes the given serviceName pidfile, using getpid(). - */ -bool -create_service_pidfile(const char *pidfile, const char *serviceName) -{ - pid_t pid = getpid(); - - /* compute the service pidfile and store it in our global variable */ - (void) get_service_pidfile(pidfile, serviceName, service_pidfile); - - /* register our service pidfile clean-up atexit */ - atexit(remove_service_pidfile_atexit); - - return create_pidfile(service_pidfile, pid); -} - - -/* - * get_service_pidfile computes the pidfile names for the given service. - */ -void -get_service_pidfile(const char *pidfile, - const char *serviceName, - char *servicePidFilename) -{ - char filename[MAXPGPATH] = { 0 }; - - sformat(filename, sizeof(filename), "pg_autoctl_%s.pid", serviceName); - path_in_same_directory(pidfile, filename, servicePidFilename); -} - - -/* - * remove_service_pidfile_atexit is called atexit() to remove the service - * pidfile. - */ -static void -remove_service_pidfile_atexit() -{ - (void) remove_pidfile(service_pidfile); -} - - -/* - * read_pidfile read pg_autoctl pid from a file, and returns true when we could - * read a PID that belongs to a currently running process. - */ -bool -read_pidfile(const char *pidfile, pid_t *pid) -{ - long fileSize = 0L; - char *fileContents = NULL; - char *fileLines[1]; - int pidnum = 0; - - if (!file_exists(pidfile)) - { - return false; - } - - if (!read_file(pidfile, &fileContents, &fileSize)) - { - log_debug("Failed to read the PID file \"%s\", removing it", pidfile); - (void) remove_pidfile(pidfile); - - return false; - } - - splitLines(fileContents, fileLines, 1); - stringToInt(fileLines[0], &pidnum); - - *pid = pidnum; - - free(fileContents); - - if (*pid <= 0) - { - log_debug("Read negative pid %d in file \"%s\", removing it", - *pid, pidfile); - (void) remove_pidfile(pidfile); - - return false; - } - - /* is it a stale file? */ - if (kill(*pid, 0) == 0) - { - return true; - } - else - { - log_debug("Failed to signal pid %d: %m", *pid); - *pid = 0; - - log_info("Found a stale pidfile at \"%s\"", pidfile); - log_warn("Removing the stale pid file \"%s\"", pidfile); - - /* - * We must return false here, after having determined that the - * pidfile belongs to a process that doesn't exist anymore. So we - * remove the pidfile and don't take the return value into account - * at this point. - */ - (void) remove_pidfile(pidfile); - - /* we might have to cleanup a stale SysV semaphore, too */ - (void) semaphore_cleanup(pidfile); - - return false; - } -} - - -/* - * remove_pidfile removes pg_autoctl pidfile. - */ -bool -remove_pidfile(const char *pidfile) -{ - if (remove(pidfile) != 0) - { - log_error("Failed to remove pid file \"%s\": %m", pidfile); - return false; - } - return true; -} - - -/* - * check_pidfile checks that the given PID file still contains the known pid of - * the service. If the file is owned by another process, just quit immediately. - */ -void -check_pidfile(const char *pidfile, pid_t start_pid) -{ - pid_t checkpid = 0; - - /* - * It might happen that the PID file got removed from disk, then - * allowing another process to run. - * - * We should then quit in an emergency if our PID file either doesn't - * exist anymore, or has been overwritten with another PID. - * - */ - if (read_pidfile(pidfile, &checkpid)) - { - if (checkpid != start_pid) - { - log_fatal("Our PID file \"%s\" now contains PID %d, " - "instead of expected pid %d. Quitting.", - pidfile, checkpid, start_pid); - - exit(EXIT_CODE_QUIT); - } - } - else - { - /* - * Surrendering seems the less risky option for us now. - * - * Any other strategy would need to be careful about race conditions - * happening when several processes (keeper or others) are trying to - * create or remove the pidfile at the same time, possibly in different - * orders. Yeah, let's quit. - */ - log_fatal("PID file not found at \"%s\", quitting.", pidfile); - exit(EXIT_CODE_QUIT); - } -} - - -/* - * read_service_pidfile_version_string reads a service pidfile and copies the - * version string found on line PIDFILE_LINE_VERSION_STRING into the - * pre-allocated buffer versionString. - */ -bool -read_service_pidfile_version_strings(const char *pidfile, - char *versionString, - char *extensionVersionString) -{ - long fileSize = 0L; - char *fileContents = NULL; - char *fileLines[BUFSIZE] = { 0 }; - int lineNumber; - - if (!read_file_if_exists(pidfile, &fileContents, &fileSize)) - { - return false; - } - - int lineCount = splitLines(fileContents, fileLines, BUFSIZE); - - for (lineNumber = 0; lineNumber < lineCount; lineNumber++) - { - int pidLine = lineNumber + 1; /* zero-based, one-based */ - - /* version string */ - if (pidLine == PIDFILE_LINE_VERSION_STRING) - { - strlcpy(versionString, fileLines[lineNumber], BUFSIZE); - } - - /* extension version string, comes later in the file */ - if (pidLine == PIDFILE_LINE_EXTENSION_VERSION) - { - strlcpy(extensionVersionString, fileLines[lineNumber], BUFSIZE); - free(fileContents); - return true; - } - } - - free(fileContents); - - return false; -} - - -/* - * fprint_pidfile_as_json prints the content of the pidfile as JSON. - * - * When includeStatus is true, add a "status" entry for each PID (main service - * and sub-processes) with either "running" or "stale" as a value, depending on - * what a kill -0 reports. - */ -void -pidfile_as_json(JSON_Value *js, const char *pidfile, bool includeStatus) -{ - JSON_Value *jsServices = json_value_init_array(); - JSON_Array *jsServicesArray = json_value_get_array(jsServices); - - JSON_Object *jsobj = json_value_get_object(js); - - long fileSize = 0L; - char *fileContents = NULL; - char *fileLines[BUFSIZE] = { 0 }; - int lineNumber; - - if (!read_file_if_exists(pidfile, &fileContents, &fileSize)) - { - exit(EXIT_CODE_INTERNAL_ERROR); - } - - int lineCount = splitLines(fileContents, fileLines, BUFSIZE); - - for (lineNumber = 0; lineNumber < lineCount; lineNumber++) - { - int pidLine = lineNumber + 1; /* zero-based, one-based */ - char *separator = NULL; - - /* main pid */ - if (pidLine == PIDFILE_LINE_PID) - { - int pidnum = 0; - stringToInt(fileLines[lineNumber], &pidnum); - json_object_set_number(jsobj, "pid", (double) pidnum); - - if (includeStatus) - { - if (kill(pidnum, 0) == 0) - { - json_object_set_string(jsobj, "status", "running"); - } - else - { - json_object_set_string(jsobj, "status", "stale"); - } - } - continue; - } - - /* data directory */ - if (pidLine == PIDFILE_LINE_DATA_DIR) - { - json_object_set_string(jsobj, "pgdata", fileLines[lineNumber]); - } - - /* version string */ - if (pidLine == PIDFILE_LINE_VERSION_STRING) - { - json_object_set_string(jsobj, "version", fileLines[lineNumber]); - } - - /* extension version string */ - if (pidLine == PIDFILE_LINE_EXTENSION_VERSION) - { - /* skip it, the supervisor does not connect to the monitor */ - (void) 0; - } - - /* semId */ - if (pidLine == PIDFILE_LINE_SEM_ID) - { - int semId = 0; - - if (stringToInt(fileLines[lineNumber], &semId)) - { - json_object_set_number(jsobj, "semId", (double) semId); - } - else - { - log_error("Failed to parse semId \"%s\"", fileLines[lineNumber]); - } - - continue; - } - - if (pidLine >= PIDFILE_LINE_FIRST_SERVICE) - { - JSON_Value *jsService = json_value_init_object(); - JSON_Object *jsServiceObj = json_value_get_object(jsService); - - if ((separator = strchr(fileLines[lineNumber], ' ')) == NULL) - { - log_debug("Failed to find a space separator in line: \"%s\"", - fileLines[lineNumber]); - continue; - } - else - { - int pidnum = 0; - char *serviceName = separator + 1; - - char servicePidFile[BUFSIZE] = { 0 }; - - char versionString[BUFSIZE] = { 0 }; - char extensionVersionString[BUFSIZE] = { 0 }; - - *separator = '\0'; - stringToInt(fileLines[lineNumber], &pidnum); - - json_object_set_string(jsServiceObj, "name", serviceName); - json_object_set_number(jsServiceObj, "pid", pidnum); - - if (includeStatus) - { - if (kill(pidnum, 0) == 0) - { - json_object_set_string(jsServiceObj, "status", "running"); - } - else - { - json_object_set_string(jsServiceObj, "status", "stale"); - } - } - - /* grab version number of the service by parsing its pidfile */ - get_service_pidfile(pidfile, serviceName, servicePidFile); - - if (!read_service_pidfile_version_strings( - servicePidFile, - versionString, - extensionVersionString)) - { - /* warn about it and continue */ - log_warn("Failed to read version string for " - "service \"%s\" in pidfile \"%s\"", - serviceName, - servicePidFile); - } - else - { - json_object_set_string(jsServiceObj, - "version", versionString); - json_object_set_string(jsServiceObj, - "pgautofailover", - extensionVersionString); - } - } - - json_array_append_value(jsServicesArray, jsService); - } - } - - json_object_set_value(jsobj, "services", jsServices); - - free(fileContents); -} - - -/* - * is_process_stopped reads given pidfile and checks if the included PID - * belongs to a process that's still running, and if not, sets the *stopped - * boolean to true. - */ -bool -is_process_stopped(const char *pidfile, bool *stopped, pid_t *pid) -{ - if (!file_exists(pidfile)) - { - *stopped = true; - return true; - } - - if (!read_pidfile(pidfile, pid)) - { - log_error("Failed to read PID file \"%s\"", pidfile); - return false; - } - - *stopped = false; - return true; -} - - -/* - * wait_for_process_to_stop waits until the PID found in the pidfile is not - * running anymore. - */ -bool -wait_for_process_to_stop(const char *pidfile, int timeout, bool *stopped, pid_t *pid) -{ - if (!is_process_stopped(pidfile, stopped, pid)) - { - /* errors have already been logged */ - return false; - } - - /* if the process has stopped already, we're done here */ - if (*stopped) - { - return true; - } - - log_info("An instance of pg_autoctl is running with PID %d, " - "waiting for it to stop.", *pid); - - int timeout_counter = timeout; - - while (timeout_counter > 0) - { - if (kill(*pid, 0) == -1 && errno == ESRCH) - { - log_info("The pg_autoctl instance with pid %d " - "has now terminated.", - *pid); - *stopped = true; - return true; - } - - sleep(1); - --timeout_counter; - } - - *stopped = false; - return true; -} diff --git a/src/bin/pg_autoctl/pidfile.h b/src/bin/pg_autoctl/pidfile.h deleted file mode 100644 index 749bfd083..000000000 --- a/src/bin/pg_autoctl/pidfile.h +++ /dev/null @@ -1,74 +0,0 @@ -/* - * src/bin/pg_autoctl/pidfile.h - * Utilities to manage the pg_autoctl pidfile. - * - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the PostgreSQL License. - * - */ -#ifndef PIDFILE_H -#define PIDFILE_H - -#include -#include - -#include "postgres_fe.h" -#include "pqexpbuffer.h" - -#include "keeper.h" -#include "keeper_config.h" -#include "monitor.h" -#include "monitor_config.h" - -/* - * As of pg_autoctl 1.4, the contents of the pidfile is: - * - * line # - * 1 supervisor PID - * 2 data directory path - * 3 version number (PG_AUTOCTL_VERSION) - * 4 extension version number (PG_AUTOCTL_EXTENSION_VERSION) - * 5 shared semaphore id (used to serialize log writes) - * 6 first supervised service pid line - * 7 second supervised service pid line - * ... - * - * The supervised service lines are added later, not the first time we create - * the pidfile. Each service line contains 2 bits of information, separated - * with spaces: - * - * pid service-name - * - * Each service creates its own pidfile with its own version number. At - * pg_autoctl upgrade time, we might have a supervisor process that's running - * with a different version than one of the restarted pg_autoctl services. - */ -#define PIDFILE_LINE_PID 1 -#define PIDFILE_LINE_DATA_DIR 2 -#define PIDFILE_LINE_VERSION_STRING 3 -#define PIDFILE_LINE_EXTENSION_VERSION 4 -#define PIDFILE_LINE_SEM_ID 5 -#define PIDFILE_LINE_FIRST_SERVICE 6 - -bool create_pidfile(const char *pidfile, pid_t pid); - -bool prepare_pidfile_buffer(PQExpBuffer content, pid_t pid); -bool create_service_pidfile(const char *pidfile, const char *serviceName); -void get_service_pidfile(const char *pidfile, - const char *serviceName, - char *filename); -bool read_service_pidfile_version_strings(const char *pidfile, - char *versionString, - char *extensionVersionString); - -bool read_pidfile(const char *pidfile, pid_t *pid); -bool remove_pidfile(const char *pidfile); -void check_pidfile(const char *pidfile, pid_t start_pid); - -void pidfile_as_json(JSON_Value *js, const char *pidfile, bool includeStatus); - -bool is_process_stopped(const char *pidfile, bool *stopped, pid_t *pid); -bool wait_for_process_to_stop(const char *pidfile, int timeout, bool *stopped, - pid_t *pid); - -#endif /* PIDFILE_H */ diff --git a/src/bin/pg_autoctl/signals.c b/src/bin/pg_autoctl/signals.c deleted file mode 100644 index 0114affe0..000000000 --- a/src/bin/pg_autoctl/signals.c +++ /dev/null @@ -1,258 +0,0 @@ -/* - * src/bin/pg_autoctl/signals.c - * Signal handlers for pg_autoctl, used in loop.c and pgsetup.c - * - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the PostgreSQL License. - * - */ - -#include -#include -#include -#include - -#include "postgres_fe.h" /* pqsignal, portable sigaction wrapper */ - -#include "cli_root.h" -#include "defaults.h" -#include "lock_utils.h" -#include "log.h" -#include "signals.h" - -/* This flag controls termination of the main loop. */ -volatile sig_atomic_t asked_to_stop = 0; /* SIGTERM */ -volatile sig_atomic_t asked_to_stop_fast = 0; /* SIGINT */ -volatile sig_atomic_t asked_to_reload = 0; /* SIGHUP */ -volatile sig_atomic_t asked_to_quit = 0; /* SIGQUIT */ - -/* - * set_signal_handlers sets our signal handlers for the 4 signals that we - * specifically handle in pg_autoctl. - */ -void -set_signal_handlers(bool exitOnQuit) -{ - /* Establish a handler for signals. */ - log_trace("set_signal_handlers%s", exitOnQuit ? " (exit on quit)" : ""); - - pqsignal(SIGHUP, catch_reload); - pqsignal(SIGINT, catch_int); - pqsignal(SIGTERM, catch_term); - - if (exitOnQuit) - { - pqsignal(SIGQUIT, catch_quit_and_exit); - } - else - { - pqsignal(SIGQUIT, catch_quit); - } -} - - -/* - * mask_signals prepares a pselect() call by masking all the signals we handle - * in this part of the code, to avoid race conditions with setting our atomic - * variables at signal handling. - */ -bool -block_signals(sigset_t *mask, sigset_t *orig_mask) -{ - int signals[] = { SIGHUP, SIGINT, SIGTERM, SIGQUIT, -1 }; - - if (sigemptyset(mask) == -1) - { - /* man sigemptyset sayth: No errors are defined. */ - log_error("sigemptyset: %m"); - return false; - } - - for (int i = 0; signals[i] != -1; i++) - { - /* - * The sigaddset() function may fail if: - * - * EINVAL The value of the signo argument is an invalid or unsupported - * signal number - * - * This should never happen given the manual set of signals we are - * processing here in this loop. - */ - if (sigaddset(mask, signals[i]) == -1) - { - log_error("sigaddset: %m"); - return false; - } - } - - if (sigprocmask(SIG_BLOCK, mask, orig_mask) == -1) - { - log_error("Failed to block signals: sigprocmask: %m"); - return false; - } - - return true; -} - - -/* - * unblock_signals calls sigprocmask to re-establish the normal signal mask, in - * order to allow our code to handle signals again. - * - * If we fail to unblock signals, then we won't be able to react to any - * interruption, reload, or shutdown sequence, and we'd rather exit now. - */ -void -unblock_signals(sigset_t *orig_mask) -{ - /* restore signal masks (un block them) now */ - if (sigprocmask(SIG_SETMASK, orig_mask, NULL) == -1) - { - log_fatal("Failed to restore signals: sigprocmask: %m"); - exit(EXIT_CODE_INTERNAL_ERROR); - } -} - - -/* - * catch_reload receives the SIGHUP signal. - */ -void -catch_reload(int sig) -{ - asked_to_reload = 1; - pqsignal(sig, catch_reload); -} - - -/* - * catch_int receives the SIGINT signal. - */ -void -catch_int(int sig) -{ - asked_to_stop_fast = 1; - pqsignal(sig, catch_int); -} - - -/* - * catch_stop receives SIGTERM signal. - */ -void -catch_term(int sig) -{ - asked_to_stop = 1; - pqsignal(sig, catch_term); -} - - -/* - * catch_quit receives the SIGQUIT signal. - */ -void -catch_quit(int sig) -{ - /* default signal handler disposition is to core dump, we don't */ - asked_to_quit = 1; - pqsignal(sig, catch_quit); -} - - -/* - * quit_and_exit exit(EXIT_CODE_QUIT) upon receiving the SIGQUIT signal. - */ -void -catch_quit_and_exit(int sig) -{ - /* default signal handler disposition is to core dump, we don't */ - exit(EXIT_CODE_QUIT); -} - - -/* - * get_current_signal returns the current signal to process and gives a prioriy - * towards SIGQUIT, then SIGINT, then SIGTERM. - */ -int -get_current_signal(int defaultSignal) -{ - if (asked_to_quit) - { - return SIGQUIT; - } - else if (asked_to_stop_fast) - { - return SIGINT; - } - else if (asked_to_stop) - { - return SIGTERM; - } - - /* no termination signal to process at this time, return the default */ - return defaultSignal; -} - - -/* - * pick_stronger_signal returns the "stronger" signal among the two given - * arguments. - * - * Signal processing have a priority or hierarchy of their own. Once we have - * received and processed SIGQUIT we want to stay at this signal level. Once we - * have received SIGINT we may upgrade to SIGQUIT, but we won't downgrade to - * SIGTERM. - */ -int -pick_stronger_signal(int sig1, int sig2) -{ - if (sig1 == SIGQUIT || sig2 == SIGQUIT) - { - return SIGQUIT; - } - else if (sig1 == SIGINT || sig2 == SIGINT) - { - return SIGINT; - } - else - { - return SIGTERM; - } -} - - -/* - * signal_to_string is our own specialised function to display a signal. The - * strsignal() output does not look like what we need. - */ -char * -signal_to_string(int signal) -{ - switch (signal) - { - case SIGQUIT: - { - return "SIGQUIT"; - } - - case SIGTERM: - { - return "SIGTERM"; - } - - case SIGINT: - { - return "SIGINT"; - } - - case SIGHUP: - { - return "SIGHUP"; - } - - default: - return "unknown signal"; - } -} diff --git a/src/bin/pg_autoctl/signals.h b/src/bin/pg_autoctl/signals.h deleted file mode 100644 index 3c6d27807..000000000 --- a/src/bin/pg_autoctl/signals.h +++ /dev/null @@ -1,38 +0,0 @@ -/* - * src/bin/pg_autoctl/signals.h - * Signal handlers for pg_autoctl, used in loop.c and pgsetup.c - * - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the PostgreSQL License. - * - */ - -#ifndef SIGNALS_H -#define SIGNALS_H - -#include -#include - -/* This flag controls termination of the main loop. */ -extern volatile sig_atomic_t asked_to_stop; /* SIGTERM */ -extern volatile sig_atomic_t asked_to_stop_fast; /* SIGINT */ -extern volatile sig_atomic_t asked_to_reload; /* SIGHUP */ -extern volatile sig_atomic_t asked_to_quit; /* SIGQUIT */ - -#define CHECK_FOR_FAST_SHUTDOWN { if (asked_to_stop_fast) { break; } \ -} - -void set_signal_handlers(bool exitOnQuit); -bool block_signals(sigset_t *mask, sigset_t *orig_mask); -void unblock_signals(sigset_t *orig_mask); -void catch_reload(int sig); -void catch_int(int sig); -void catch_term(int sig); -void catch_quit(int sig); -void catch_quit_and_exit(int sig); - -int get_current_signal(int defaultSignal); -int pick_stronger_signal(int sig1, int sig2); -char * signal_to_string(int signal); - -#endif /* SIGNALS_H */ diff --git a/src/bin/pg_autoctl/string_utils.c b/src/bin/pg_autoctl/string_utils.c deleted file mode 100644 index c9d38f2c2..000000000 --- a/src/bin/pg_autoctl/string_utils.c +++ /dev/null @@ -1,601 +0,0 @@ -/* - * src/bin/pg_autoctl/string_utils.c - * Implementations of utility functions for string handling - * - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the PostgreSQL License. - * - */ - -#include -#include -#include -#include -#include -#include -#include - -#include "postgres_fe.h" -#include "pqexpbuffer.h" - -#include "defaults.h" -#include "file_utils.h" -#include "log.h" -#include "parsing.h" -#include "string_utils.h" - -/* - * intToString converts an int to an IntString, which contains a decimal string - * representation of the integer. - */ -IntString -intToString(int64_t number) -{ - IntString intString; - - intString.intValue = number; - - sformat(intString.strValue, INTSTRING_MAX_DIGITS, "%" PRId64, number); - - return intString; -} - - -/* - * converts given string to 64 bit integer value. - * returns 0 upon failure and sets error flag - */ -bool -stringToInt(const char *str, int *number) -{ - char *endptr; - - if (str == NULL) - { - return false; - } - - if (number == NULL) - { - return false; - } - - errno = 0; - - long long int n = strtoll(str, &endptr, 10); - - if (str == endptr) - { - return false; - } - else if (errno != 0) - { - return false; - } - else if (*endptr != '\0') - { - return false; - } - else if (n < INT_MIN || n > INT_MAX) - { - return false; - } - - *number = n; - - return true; -} - - -/* - * converts given string to 64 bit integer value. - * returns 0 upon failure and sets error flag - */ -bool -stringToInt64(const char *str, int64_t *number) -{ - char *endptr; - - if (str == NULL) - { - return false; - } - - if (number == NULL) - { - return false; - } - - errno = 0; - - long long int n = strtoll(str, &endptr, 10); - - if (str == endptr) - { - return false; - } - else if (errno != 0) - { - return false; - } - else if (*endptr != '\0') - { - return false; - } - else if (n < INT64_MIN || n > INT64_MAX) - { - return false; - } - - *number = n; - - return true; -} - - -/* - * converts given string to 64 bit unsigned integer value. - * returns 0 upon failure and sets error flag - */ -bool -stringToUInt(const char *str, unsigned int *number) -{ - char *endptr; - - if (str == NULL) - { - return false; - } - - if (number == NULL) - { - return false; - } - - errno = 0; - unsigned long long n = strtoull(str, &endptr, 10); - - if (str == endptr) - { - return false; - } - else if (errno != 0) - { - return false; - } - else if (*endptr != '\0') - { - return false; - } - else if (n > UINT_MAX) - { - return false; - } - - *number = n; - - return true; -} - - -/* - * converts given string to 64 bit unsigned integer value. - * returns 0 upon failure and sets error flag - */ -bool -stringToUInt64(const char *str, uint64_t *number) -{ - char *endptr; - - if (str == NULL) - { - return false; - } - - if (number == NULL) - { - return false; - } - - errno = 0; - unsigned long long n = strtoull(str, &endptr, 10); - - if (str == endptr) - { - return false; - } - else if (errno != 0) - { - return false; - } - else if (*endptr != '\0') - { - return false; - } - else if (n > UINT64_MAX) - { - return false; - } - - *number = n; - - return true; -} - - -/* - * converts given string to short value. - * returns 0 upon failure and sets error flag - */ -bool -stringToShort(const char *str, short *number) -{ - char *endptr; - - if (str == NULL) - { - return false; - } - - if (number == NULL) - { - return false; - } - - errno = 0; - - long long int n = strtoll(str, &endptr, 10); - - if (str == endptr) - { - return false; - } - else if (errno != 0) - { - return false; - } - else if (*endptr != '\0') - { - return false; - } - else if (n < SHRT_MIN || n > SHRT_MAX) - { - return false; - } - - *number = n; - - return true; -} - - -/* - * converts given string to unsigned short value. - * returns 0 upon failure and sets error flag - */ -bool -stringToUShort(const char *str, unsigned short *number) -{ - char *endptr; - - if (str == NULL) - { - return false; - } - - if (number == NULL) - { - return false; - } - - errno = 0; - unsigned long long n = strtoull(str, &endptr, 10); - - if (str == endptr) - { - return false; - } - else if (errno != 0) - { - return false; - } - else if (*endptr != '\0') - { - return false; - } - else if (n > USHRT_MAX) - { - return false; - } - - *number = n; - - return true; -} - - -/* - * converts given string to 32 bit integer value. - * returns 0 upon failure and sets error flag - */ -bool -stringToInt32(const char *str, int32_t *number) -{ - char *endptr; - - if (str == NULL) - { - return false; - } - - if (number == NULL) - { - return false; - } - - errno = 0; - - long long int n = strtoll(str, &endptr, 10); - - if (str == endptr) - { - return false; - } - else if (errno != 0) - { - return false; - } - else if (*endptr != '\0') - { - return false; - } - else if (n < INT32_MIN || n > INT32_MAX) - { - return false; - } - - *number = n; - - return true; -} - - -/* - * converts given string to 32 bit unsigned int value. - * returns 0 upon failure and sets error flag - */ -bool -stringToUInt32(const char *str, uint32_t *number) -{ - char *endptr; - - if (str == NULL) - { - return false; - } - - if (number == NULL) - { - return false; - } - - errno = 0; - unsigned long long n = strtoull(str, &endptr, 10); - - if (str == endptr) - { - return false; - } - else if (errno != 0) - { - return false; - } - else if (*endptr != '\0') - { - return false; - } - else if (n > UINT32_MAX) - { - return false; - } - - *number = n; - - return true; -} - - -/* - * converts given string to a double precision float value. - * returns 0 upon failure and sets error flag - */ -bool -stringToDouble(const char *str, double *number) -{ - char *endptr; - - if (str == NULL) - { - return false; - } - - if (number == NULL) - { - return false; - } - - errno = 0; - double n = strtod(str, &endptr); - - if (str == endptr) - { - return false; - } - else if (errno != 0) - { - return false; - } - else if (*endptr != '\0') - { - return false; - } - else if (n > DBL_MAX) - { - return false; - } - - *number = n; - - return true; -} - - -/* - * IntervalToString prepares a string buffer to represent a given interval - * value given as a double precision float number. - */ -bool -IntervalToString(double seconds, char *buffer, size_t size) -{ - if (seconds < 1.0) - { - /* when we have < 1s, we round to 1s */ - sformat(buffer, size, " %ds", 1); - } - else if (seconds < 60.0) - { - int s = (int) seconds; - - sformat(buffer, size, "%2ds", s); - } - else if (seconds < (60.0 * 60.0)) - { - int mins = (int) (seconds / 60.0); - int secs = (int) (seconds - (mins * 60.0)); - - sformat(buffer, size, "%2dm%02ds", mins, secs); - } - else if (seconds < (24.0 * 60.0 * 60.0)) - { - int hours = (int) (seconds / (60.0 * 60.0)); - int mins = (int) ((seconds - (hours * 60.0 * 60.0)) / 60.0); - - sformat(buffer, size, "%2dh%02dm", hours, mins); - } - else - { - int days = (int) (seconds / (24.0 * 60.0 * 60.0)); - int hours = - (int) ((seconds - (days * 24.0 * 60.0 * 60.0)) / (60.0 * 60.0)); - - sformat(buffer, size, "%2dd%02dh", days, hours); - } - - return true; -} - - -/* - * countLines returns how many line separators (\n) are found in the given - * string. - */ -int -countLines(char *buffer) -{ - int lineNumber = 0; - char *currentLine = buffer; - - if (buffer == NULL) - { - return 0; - } - - do { - char *newLinePtr = strchr(currentLine, '\n'); - - if (newLinePtr == NULL) - { - if (strlen(currentLine) > 0) - { - ++lineNumber; - } - currentLine = NULL; - } - else - { - ++lineNumber; - currentLine = ++newLinePtr; - } - } while (currentLine != NULL && *currentLine != '\0'); - - return lineNumber; -} - - -/* - * splitLines prepares a multi-line error message in a way that calling code - * can loop around one line at a time and call log_error() or log_warn() on - * individual lines. - */ -int -splitLines(char *errorMessage, char **linesArray, int size) -{ - int lineNumber = 0; - char *currentLine = errorMessage; - - if (errorMessage == NULL) - { - return 0; - } - - do { - char *newLinePtr = strchr(currentLine, '\n'); - - if (newLinePtr == NULL) - { - if (strlen(currentLine) > 0) - { - linesArray[lineNumber++] = currentLine; - } - - currentLine = NULL; - } - else - { - *newLinePtr = '\0'; - - linesArray[lineNumber++] = currentLine; - - currentLine = ++newLinePtr; - } - } while (currentLine != NULL && *currentLine != '\0' && lineNumber < size); - - return lineNumber; -} - - -/* - * processBufferCallback is a function callback to use with the subcommands.c - * library when we want to output a command's output as it's running, such as - * when running a pg_basebackup command. - */ -void -processBufferCallback(const char *buffer, bool error) -{ - char *outLines[BUFSIZE] = { 0 }; - int lineCount = splitLines((char *) buffer, outLines, BUFSIZE); - int lineNumber = 0; - - for (lineNumber = 0; lineNumber < lineCount; lineNumber++) - { - if (strneq(outLines[lineNumber], "")) - { - /* - * pg_basebackup and other utilities write their progress output on - * stderr, we don't want to have ERROR message when it's all good. - * As a result we always target INFO log level here. - */ - log_info("%s", outLines[lineNumber]); - } - } -} diff --git a/src/bin/pg_autoctl/string_utils.h b/src/bin/pg_autoctl/string_utils.h deleted file mode 100644 index 928e6defb..000000000 --- a/src/bin/pg_autoctl/string_utils.h +++ /dev/null @@ -1,44 +0,0 @@ -/* - * src/bin/pg_autoctl/string_utils.h - * Utility functions for string handling - * - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the PostgreSQL License. - * - */ -#ifndef STRING_UTILS_H -#define STRING_UTILS_H - -#include - - -/* maximum decimal int64 length with minus and NUL */ -#define INTSTRING_MAX_DIGITS 21 -typedef struct IntString -{ - int64_t intValue; - char strValue[INTSTRING_MAX_DIGITS]; -} IntString; - -IntString intToString(int64_t number); - -bool stringToInt(const char *str, int *number); -bool stringToUInt(const char *str, unsigned int *number); - -bool stringToInt64(const char *str, int64_t *number); -bool stringToUInt64(const char *str, uint64_t *number); - -bool stringToShort(const char *str, short *number); -bool stringToUShort(const char *str, unsigned short *number); - -bool stringToInt32(const char *str, int32_t *number); -bool stringToUInt32(const char *str, uint32_t *number); - -bool stringToDouble(const char *str, double *number); -bool IntervalToString(double seconds, char *buffer, size_t size); - -int countLines(char *buffer); -int splitLines(char *errorMessage, char **linesArray, int size); -void processBufferCallback(const char *buffer, bool error); - -#endif /* STRING_UTILS_h */ diff --git a/src/bin/pg_autoctl/system_utils.c b/src/bin/pg_autoctl/system_utils.c deleted file mode 100644 index c19dfa10b..000000000 --- a/src/bin/pg_autoctl/system_utils.c +++ /dev/null @@ -1,145 +0,0 @@ -/* - * src/bin/pg_autoctl/hardware_utils.c - * Utility functions for getting CPU and Memory information. - * - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the PostgreSQL License. - * - */ - -#if defined(__linux__) -#include -#else -#include -#include -#include -#endif - -#include - -#include "log.h" -#include "file_utils.h" -#include "system_utils.h" - -#if defined(__linux__) -static bool get_system_info_linux(SystemInfo *sysInfo); -#endif - -#if defined(__APPLE__) || defined(BSD) -static bool get_system_info_bsd(SystemInfo *sysInfo); -#endif - -/* - * get_system_info probes for system information and fills the given SystemInfo - * structure with what we found: number of CPUs and total amount of memory. - */ -bool -get_system_info(SystemInfo *sysInfo) -{ -#if defined(__APPLE__) || defined(BSD) - return get_system_info_bsd(sysInfo); -#elif defined(__linux__) - return get_system_info_linux(sysInfo); -#else - log_error("Failed to get system information: " - "Operating System not supported"); - return false; -#endif -} - - -/* - * On Linux, use sysinfo(2) and getnprocs(3) - */ -#if defined(__linux__) -static bool -get_system_info_linux(SystemInfo *sysInfo) -{ - struct sysinfo linuxSysInfo = { 0 }; - - if (sysinfo(&linuxSysInfo) != 0) - { - log_error("Failed to call sysinfo(): %m"); - return false; - } - - sysInfo->ncpu = get_nprocs(); - sysInfo->totalram = linuxSysInfo.totalram; - - return true; -} - - -#endif - - -/* - * FreeBSD, OpenBSD, and darwin use the sysctl(3) API. - */ -#if defined(__APPLE__) || defined(BSD) -static bool -get_system_info_bsd(SystemInfo *sysInfo) -{ - unsigned int ncpu = 0; /* the API requires an integer here */ - int ncpuMIB[2] = { CTL_HW, HW_NCPU }; - #if defined(HW_MEMSIZE) - int ramMIB[2] = { CTL_HW, HW_MEMSIZE }; /* MacOS */ - #elif defined(HW_PHYSMEM64) - int ramMIB[2] = { CTL_HW, HW_PHYSMEM64 }; /* OpenBSD */ - #else - int ramMIB[2] = { CTL_HW, HW_PHYSMEM }; /* FreeBSD */ - #endif - - size_t cpuSize = sizeof(ncpu); - size_t memSize = sizeof(sysInfo->totalram); - - if (sysctl(ncpuMIB, 2, &ncpu, &cpuSize, NULL, 0) == -1) - { - log_error("Failed to probe number of CPUs: %m"); - return false; - } - - sysInfo->ncpu = (unsigned short) ncpu; - - if (sysctl(ramMIB, 2, &(sysInfo->totalram), &memSize, NULL, 0) == -1) - { - log_error("Failed to probe Physical Memory: %m"); - return false; - } - - return true; -} - - -#endif - - -/* - * pretty_print_bytes pretty prints bytes in a human readable form. Given - * 17179869184 it places the string "16 GB" in the given buffer. - */ -void -pretty_print_bytes(char *buffer, size_t size, uint64_t bytes) -{ - const char *suffixes[7] = { - "B", /* Bytes */ - "kB", /* Kilo */ - "MB", /* Mega */ - "GB", /* Giga */ - "TB", /* Tera */ - "PB", /* Peta */ - "EB" /* Exa */ - }; - - uint sIndex = 0; - long double count = bytes; - - while (count >= 10240 && sIndex < 7) - { - sIndex++; - count /= 1024; - } - - /* forget about having more precision, Postgres wants integers here */ - sformat(buffer, size, "%d %s", (int) count, suffixes[sIndex]); -} diff --git a/src/bin/pg_autoctl/system_utils.h b/src/bin/pg_autoctl/system_utils.h deleted file mode 100644 index 64dd2fa1d..000000000 --- a/src/bin/pg_autoctl/system_utils.h +++ /dev/null @@ -1,27 +0,0 @@ -/* - * src/bin/pg_autoctl/system_utils.h - * Utility functions for getting CPU and Memory information. - * - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the PostgreSQL License. - * - */ - -#ifndef SYSTEM_UTILS_H -#define SYSTEM_UTILS_H - -#include - - -/* taken from sysinfo(2) on Linux */ -typedef struct SystemInfo -{ - uint64_t totalram; /* Total usable main memory size */ - unsigned short ncpu; /* Number of current processes */ -} SystemInfo; - -bool get_system_info(SystemInfo *sysInfo); -void pretty_print_bytes(char *buffer, size_t size, uint64_t bytes); - - -#endif /* SYSTEM_UTILS_H */ From 1089854c9cc4c12bd4efe3e8784e51a6fd950800 Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Tue, 7 Jul 2026 02:18:54 +0200 Subject: [PATCH 23/68] ci: fix banned function check for pgaftest files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace strncpy/strncat with strlcpy/strlcat in hand-written pgaftest source files (test_runner.c, cli_root.c, cli_indent.c, test_spec_parse.c), and add IGNORE-BANNED to unavoidable memcpy calls. Exclude src/bin/pgaftest/ from citus-style gitattribute so that the banned.h.sh check does not flag pgaftest's legitimate use of standard C I/O (fopen, fprintf, snprintf, getenv, atoi) — which is appropriate for developer test infrastructure but banned in production code. The Flex/Bison generated files (test_spec_scan.c, test_spec_parse.c) are covered by the same exclusion. --- .gitattributes | 1 + src/bin/pgaftest/cli_indent.c | 19 ++++++-------- src/bin/pgaftest/cli_root.c | 39 +++++++++++---------------- src/bin/pgaftest/test_runner.c | 42 ++++++++++++++---------------- src/bin/pgaftest/test_spec_parse.c | 10 +++---- 5 files changed, 49 insertions(+), 62 deletions(-) diff --git a/.gitattributes b/.gitattributes index 546a2a29f..82586159d 100644 --- a/.gitattributes +++ b/.gitattributes @@ -25,3 +25,4 @@ src/bin/lib/libs/** -citus-style src/bin/lib/pg/** -citus-style src/bin/lib/subcommands.c/** -citus-style src/monitor/version_compat.c -citus-style +src/bin/pgaftest/** -citus-style diff --git a/src/bin/pgaftest/cli_indent.c b/src/bin/pgaftest/cli_indent.c index ce4a79ae2..1add741d1 100644 --- a/src/bin/pgaftest/cli_indent.c +++ b/src/bin/pgaftest/cli_indent.c @@ -208,8 +208,7 @@ print_header(FILE *out, const char *path) if (any) { /* accumulate blank lines; flush only if more comments follow */ - strncat(pending_blank, line, - sizeof(pending_blank) - strlen(pending_blank) - 1); + strlcat(pending_blank, line, sizeof(pending_blank)); } continue; } @@ -274,7 +273,7 @@ collect_comments(const char *path) if (line[0] == '#') { /* Accumulate comment line */ - strncat(pending, line, sizeof(pending) - strlen(pending) - 1); + strlcat(pending, line, sizeof(pending)); continue; } @@ -283,7 +282,7 @@ collect_comments(const char *path) /* Blank line: keep pending — comment may continue after blank */ if (pending[0]) { - strncat(pending, line, sizeof(pending) - strlen(pending) - 1); + strlcat(pending, line, sizeof(pending)); } continue; } @@ -480,14 +479,12 @@ print_node(FILE *out, const TestNode *n, int baseIndent) char inline_props[512] = ""; for (int i = 0; i < pc; i++) { - strncat(inline_props, " ", sizeof(inline_props) - strlen(inline_props) - 1); - strncat(inline_props, props[i].kw, sizeof(inline_props) - strlen(inline_props) - - 1); + strlcat(inline_props, " ", sizeof(inline_props)); + strlcat(inline_props, props[i].kw, sizeof(inline_props)); if (props[i].val[0]) { - strncat(inline_props, " ", sizeof(inline_props) - strlen(inline_props) - 1); - strncat(inline_props, props[i].val, sizeof(inline_props) - strlen( - inline_props) - 1); + strlcat(inline_props, " ", sizeof(inline_props)); + strlcat(inline_props, props[i].val, sizeof(inline_props)); } } @@ -783,7 +780,7 @@ print_sql_body(FILE *out, const char *raw_sql, int bodyIndent) if (seglen > 0) { char seg[8192]; - memcpy(seg, seg_start, (size_t) seglen); + memcpy(seg, seg_start, (size_t) seglen); /* IGNORE-BANNED */ seg[seglen] = '\0'; emit_segment(out, seg, bodyIndent); } diff --git a/src/bin/pgaftest/cli_root.c b/src/bin/pgaftest/cli_root.c index b35cfd6e7..acb845074 100644 --- a/src/bin/pgaftest/cli_root.c +++ b/src/bin/pgaftest/cli_root.c @@ -59,7 +59,7 @@ derive_work_dir(const char *specPath, char *buf, int buflen) base = base ? base + 1 : specPath; char name[256] = { 0 }; - strncpy(name, base, sizeof(name) - 1); + strlcpy(name, base, sizeof(name)); /* strip .pgaf extension */ char *dot = strrchr(name, '.'); @@ -102,22 +102,19 @@ pgaftest_getopts(int argc, char **argv) { case 'w': { - strncpy(pgaftestOpts.workDir, optarg, - sizeof(pgaftestOpts.workDir) - 1); + strlcpy(pgaftestOpts.workDir, optarg, sizeof(pgaftestOpts.workDir)); break; } case 'S': { - strncpy(pgaftestOpts.schedule, optarg, - sizeof(pgaftestOpts.schedule) - 1); + strlcpy(pgaftestOpts.schedule, optarg, sizeof(pgaftestOpts.schedule)); break; } case 'E': { - strncpy(pgaftestOpts.expected, optarg, - sizeof(pgaftestOpts.expected) - 1); + strlcpy(pgaftestOpts.expected, optarg, sizeof(pgaftestOpts.expected)); break; } @@ -218,7 +215,7 @@ cli_run(int argc, char **argv) char specPath[1024]; if (strchr(p, '/')) { - strncpy(specPath, p, sizeof(specPath) - 1); + strlcpy(specPath, p, sizeof(specPath)); } else { @@ -233,7 +230,7 @@ cli_run(int argc, char **argv) const char *base = strrchr(specPath, '/'); base = base ? base + 1 : specPath; char name[128]; - strncpy(name, base, sizeof(name) - 1); + strlcpy(name, base, sizeof(name)); char *dot = strrchr(name, '.'); if (dot) { @@ -275,8 +272,7 @@ cli_run(int argc, char **argv) exit(1); } - strncpy(pgaftestOpts.specFile, argv[0], - sizeof(pgaftestOpts.specFile) - 1); + strlcpy(pgaftestOpts.specFile, argv[0], sizeof(pgaftestOpts.specFile)); if (pgaftestOpts.workDir[0] == '\0') { @@ -307,8 +303,7 @@ cli_setup(int argc, char **argv) exit(1); } - strncpy(pgaftestOpts.specFile, argv[0], - sizeof(pgaftestOpts.specFile) - 1); + strlcpy(pgaftestOpts.specFile, argv[0], sizeof(pgaftestOpts.specFile)); /* * The commandline library stops getopt at the first non-option (the spec @@ -354,8 +349,7 @@ cli_step(int argc, char **argv) } /* step name is positional */ - strncpy(pgaftestOpts.stepName, argv[0], - sizeof(pgaftestOpts.stepName) - 1); + strlcpy(pgaftestOpts.stepName, argv[0], sizeof(pgaftestOpts.stepName)); /* We need the spec file too — look for it in workDir */ char specPath[1024]; @@ -365,7 +359,7 @@ cli_step(int argc, char **argv) /* If there's a second positional arg, treat it as the spec path */ if (argc >= 2 && argv[1] != NULL) { - strncpy(specPath, argv[1], sizeof(specPath) - 1); + strlcpy(specPath, argv[1], sizeof(specPath)); } /* derive work dir from spec path when not given explicitly */ @@ -402,7 +396,7 @@ cli_prepare(int argc, char **argv) exit(1); } - strncpy(pgaftestOpts.specFile, argv[0], sizeof(pgaftestOpts.specFile) - 1); + strlcpy(pgaftestOpts.specFile, argv[0], sizeof(pgaftestOpts.specFile)); const char *outDir = (argc >= 2 && argv[1] && argv[1][0] != '-') ? argv[1] : NULL; @@ -430,8 +424,7 @@ cli_show(int argc, char **argv) exit(1); } - strncpy(pgaftestOpts.specFile, argv[0], - sizeof(pgaftestOpts.specFile) - 1); + strlcpy(pgaftestOpts.specFile, argv[0], sizeof(pgaftestOpts.specFile)); TestSpec *spec = parse_test_spec(pgaftestOpts.specFile); if (!spec) @@ -453,8 +446,7 @@ cli_down(int argc, char **argv) /* optional positional: spec file to derive work dir from */ if (argc >= 1 && argv[0] && argv[0][0] != '-') { - strncpy(pgaftestOpts.specFile, argv[0], - sizeof(pgaftestOpts.specFile) - 1); + strlcpy(pgaftestOpts.specFile, argv[0], sizeof(pgaftestOpts.specFile)); } if (pgaftestOpts.workDir[0] == '\0' && pgaftestOpts.specFile[0] != '\0') @@ -467,7 +459,7 @@ cli_down(int argc, char **argv) char specPath[1024]; if (pgaftestOpts.specFile[0] != '\0') { - strncpy(specPath, pgaftestOpts.specFile, sizeof(specPath) - 1); + strlcpy(specPath, pgaftestOpts.specFile, sizeof(specPath)); } else { @@ -527,8 +519,7 @@ cli_run_setup_only(int argc, char **argv) exit(1); } - strncpy(pgaftestOpts.specFile, argv[0], - sizeof(pgaftestOpts.specFile) - 1); + strlcpy(pgaftestOpts.specFile, argv[0], sizeof(pgaftestOpts.specFile)); #ifdef __BSD_VISIBLE optreset = 1; diff --git a/src/bin/pgaftest/test_runner.c b/src/bin/pgaftest/test_runner.c index 087441d06..ab29dea1f 100644 --- a/src/bin/pgaftest/test_runner.c +++ b/src/bin/pgaftest/test_runner.c @@ -120,7 +120,7 @@ runner_init(TestRunner *r, TestSpec *spec, const char *workDir) { const char *base = strrchr(spec->filename, '/'); base = base ? base + 1 : spec->filename; - strncpy(r->projectName, base, sizeof(r->projectName) - 1); + strlcpy(r->projectName, base, sizeof(r->projectName)); char *dot = strrchr(r->projectName, '.'); if (dot) { @@ -264,7 +264,7 @@ runner_compose_generate(TestRunner *r) /* ensure workdir exists (create intermediate directories as needed) */ { char tmp[sizeof(r->workDir)]; - strncpy(tmp, r->workDir, sizeof(tmp) - 1); + strlcpy(tmp, r->workDir, sizeof(tmp)); for (char *p = tmp + 1; *p; p++) { if (*p == '/') @@ -526,13 +526,13 @@ monitor_get_node_state(TestRunner *r, const char *nodeName, return false; } *sep1 = '\0'; - strncpy(reported, out, replen - 1); + strlcpy(reported, out, replen); char *sep2 = strchr(sep1 + 1, '|'); if (sep2) { *sep2 = '\0'; } - strncpy(assigned, sep1 + 1, asslen - 1); + strlcpy(assigned, sep1 + 1, asslen); return true; } @@ -1196,19 +1196,19 @@ wait_for_states(TestRunner *r, TestCmd *cmd) { if (i > 0) { - strncat(label, ", ", sizeof(label) - strlen(label) - 1); + strlcat(label, ", ", sizeof(label)); } - strncat(label, cmd->waitStates[i], sizeof(label) - strlen(label) - 1); + strlcat(label, cmd->waitStates[i], sizeof(label)); } if (cmd->waitGroupCount > 0) { - strncat(label, " in group(s) ", sizeof(label) - strlen(label) - 1); + strlcat(label, " in group(s) ", sizeof(label)); for (int i = 0; i < cmd->waitGroupCount; i++) { char g[16]; snprintf(g, sizeof(g), "%s%d", i > 0 ? "," : "", cmd->waitGroups[i]); - strncat(label, g, sizeof(label) - strlen(label) - 1); + strlcat(label, g, sizeof(label)); } } @@ -1885,7 +1885,7 @@ parse_sqlstate(const char *output, char *state, int statelen) if (valid) { int n = statelen < 6 ? statelen - 1 : 5; - memcpy(state, p, n); + memcpy(state, p, n); /* IGNORE-BANNED */ state[n] = '\0'; return; } @@ -2021,7 +2021,7 @@ runner_expand_macros(TestRunner *r, const char *args, char *dst, int dstlen, int cl = strlen(cidr); if (di + cl < dstlen - 1) { - memcpy(dst + di, cidr, cl); + memcpy(dst + di, cidr, cl); /* IGNORE-BANNED */ di += cl; } p += strlen(macro); @@ -2207,10 +2207,9 @@ runner_exec_cmd(TestRunner *r, TestCmd *cmd, char *errBuf, int errLen) { if (i > 0) { - strncat(label, ",", sizeof(label) - strlen(label) - 1); + strlcat(label, ",", sizeof(label)); } - strncat(label, cmd->waitStates[i], - sizeof(label) - strlen(label) - 1); + strlcat(label, cmd->waitStates[i], sizeof(label)); } snprintf(errBuf, errLen, "timeout: formation never reached states {%s}", label); @@ -2645,12 +2644,12 @@ runner_exec_cmd(TestRunner *r, TestCmd *cmd, char *errBuf, int errLen) { if (i > 0) { - strncat(label, " and ", sizeof(label) - strlen(label) - 1); + strlcat(label, " and ", sizeof(label)); } char part[128]; snprintf(part, sizeof(part), "%s=%s", cmd->waitNodes[i], cmd->waitStates[i]); - strncat(label, part, sizeof(label) - strlen(label) - 1); + strlcat(label, part, sizeof(label)); } snprintf(errBuf, errLen, "timeout: conditions not met: %s", label); @@ -3086,9 +3085,9 @@ cmd_label(const TestCmd *cmd, char *buf, int len) { if (i > 0) { - strncat(states, ", ", sizeof(states) - strlen(states) - 1); + strlcat(states, ", ", sizeof(states)); } - strncat(states, cmd->waitStates[i], sizeof(states) - strlen(states) - 1); + strlcat(states, cmd->waitStates[i], sizeof(states)); } snprintf(buf, len, "wait until %s timeout %ds", states, cmd->timeoutSeconds); @@ -3115,10 +3114,9 @@ cmd_label(const TestCmd *cmd, char *buf, int len) { if (i > 0) { - strncat(nodes, ", ", sizeof(nodes) - strlen(nodes) - 1); + strlcat(nodes, ", ", sizeof(nodes)); } - strncat(nodes, cmd->promoteNodes[i], - sizeof(nodes) - strlen(nodes) - 1); + strlcat(nodes, cmd->promoteNodes[i], sizeof(nodes)); } snprintf(buf, len, "promote %s", nodes[0] ? nodes : cmd->service); break; @@ -3231,12 +3229,12 @@ cmd_label(const TestCmd *cmd, char *buf, int len) { if (i > 0) { - strncat(parts, " and ", sizeof(parts) - strlen(parts) - 1); + strlcat(parts, " and ", sizeof(parts)); } char piece[128]; snprintf(piece, sizeof(piece), "%s state = %s", cmd->waitNodes[i], cmd->waitStates[i]); - strncat(parts, piece, sizeof(parts) - strlen(parts) - 1); + strlcat(parts, piece, sizeof(parts)); } snprintf(buf, len, "wait until %s timeout %ds", parts, cmd->timeoutSeconds); diff --git a/src/bin/pgaftest/test_spec_parse.c b/src/bin/pgaftest/test_spec_parse.c index 4d52d2e70..b7f3b6e33 100644 --- a/src/bin/pgaftest/test_spec_parse.c +++ b/src/bin/pgaftest/test_spec_parse.c @@ -438,7 +438,7 @@ expand_tuple_expect(char *buf, int buflen) } if (l > 0) { - memcpy(tmp + pos, row, l); + memcpy(tmp + pos, row, l); /* IGNORE-BANNED */ pos += l; } tmp[pos] = '\0'; @@ -447,7 +447,7 @@ expand_tuple_expect(char *buf, int buflen) if (!first) { - strncpy(buf, tmp, buflen - 1); + strlcpy(buf, tmp, buflen); } } @@ -2683,7 +2683,7 @@ yyparse() #line 653 "test_spec_parse.y" { TestStep *s = (yyvsp[(3) - (3)].step); - strncpy(s->name, (yyvsp[(2) - (3)].str), sizeof(s->name) - 1); + strlcpy(s->name, (yyvsp[(2) - (3)].str), sizeof(s->name)); free((yyvsp[(2) - (3)].str)); register_step(current_spec, s); } @@ -4016,7 +4016,7 @@ parse_test_spec(const char *filename) exit(1); } - strncpy(spec->filename, filename, sizeof(spec->filename) - 1); + strlcpy(spec->filename, filename, sizeof(spec->filename)); current_spec = spec; pgaf_line_number = 1; @@ -4054,7 +4054,7 @@ make_step(const char *name) } if (name) { - strncpy(s->name, name, sizeof(s->name) - 1); + strlcpy(s->name, name, sizeof(s->name)); } return s; } From e88009bdaf96c22351ae1b491927215700129b5b Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Tue, 7 Jul 2026 02:22:46 +0200 Subject: [PATCH 24/68] Dockerfile: revert CITUSTAG to v12.1.5 to restore PG14 support v13.0.1 dropped PostgreSQL 14 support (configure: error: Citus is not compatible with the detected PostgreSQL version 14). v12.1.5 is what main uses and supports PG14 through PG17. --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 4222e73f0..32229a30b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -85,7 +85,7 @@ RUN echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers FROM base AS citus ARG PGVERSION -ARG CITUSTAG=v13.0.1 +ARG CITUSTAG=v12.1.5 ENV PG_CONFIG=/usr/lib/postgresql/${PGVERSION}/bin/pg_config From bff39c8e66c944e24a35b2d06d81abfbdfe03d18 Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Tue, 7 Jul 2026 03:12:35 +0200 Subject: [PATCH 25/68] ci: fix banned functions in pgaftest using project-approved wrappers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace banned C stdlib calls with the project's safe wrappers: - snprintf() → sformat() (wraps pg_vsnprintf; from file_utils.h) - fprintf() → fformat() (wraps pg_vfprintf; from file_utils.h) - vsnprintf() → pg_vsnprintf() - printf() → fformat(stdout, ...) - atoi() → stringToInt() (from string_utils.h) - strncat() → strlcat() (already done in earlier commit) - strncpy() → strlcpy() (already done in earlier commit) For calls with no safe-wrapper alternative (fopen, getenv, sscanf, memcpy) that already follow the documented safe-use pattern, add /* IGNORE-BANNED */ on the calling line — matching the approach used in pg_autoctl/nodespec.c and common/env_utils.c. The Bison/Flex generated files (test_spec_parse.c, test_spec_scan.c) use banned functions inside Bison/Flex boilerplate; those lines get /* IGNORE-BANNED */ since we do not regenerate the grammar mid-build. Add #include "file_utils.h" and #include "string_utils.h" to the files that now call sformat/fformat/stringToInt. Remove the src/bin/pgaftest/** -citus-style .gitattributes line added in the previous commit; pgaftest is now clean under both citus_indent and ci/banned.h.sh. --- .gitattributes | 1 - src/bin/pgaftest/cli_indent.c | 195 ++++++------ src/bin/pgaftest/cli_root.c | 34 +- src/bin/pgaftest/compose_gen.c | 166 +++++----- src/bin/pgaftest/test_runner.c | 487 +++++++++++++++-------------- src/bin/pgaftest/test_spec_parse.c | 57 ++-- src/bin/pgaftest/test_spec_scan.c | 21 +- 7 files changed, 487 insertions(+), 474 deletions(-) diff --git a/.gitattributes b/.gitattributes index 82586159d..546a2a29f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -25,4 +25,3 @@ src/bin/lib/libs/** -citus-style src/bin/lib/pg/** -citus-style src/bin/lib/subcommands.c/** -citus-style src/monitor/version_compat.c -citus-style -src/bin/pgaftest/** -citus-style diff --git a/src/bin/pgaftest/cli_indent.c b/src/bin/pgaftest/cli_indent.c index 1add741d1..e36c38347 100644 --- a/src/bin/pgaftest/cli_indent.c +++ b/src/bin/pgaftest/cli_indent.c @@ -26,6 +26,7 @@ #include "test_spec.h" #include "test_spec_parse.h" #include "defaults.h" +#include "file_utils.h" #include "log.h" #include "string_utils.h" @@ -70,7 +71,7 @@ cli_indent(int argc, char **argv) { if (argc < 1 || argv[0] == NULL) { - fprintf(stderr, "Usage: pgaftest indent \n"); + fformat(stderr, "Usage: pgaftest indent \n"); exit(EXIT_CODE_BAD_ARGS); } @@ -96,14 +97,14 @@ cli_indent(int argc, char **argv) const char *cmt = lookup_comment(cmap, "setup"); if (cmt) { - fprintf(out, "\n%s\nsetup {\n", cmt); + fformat(out, "\n%s\nsetup {\n", cmt); } else { - fprintf(out, "\nsetup {\n"); + fformat(out, "\nsetup {\n"); } print_step(out, spec->setup, false); - fprintf(out, "}\n"); + fformat(out, "}\n"); } if (spec->teardown) @@ -111,14 +112,14 @@ cli_indent(int argc, char **argv) const char *cmt = lookup_comment(cmap, "teardown"); if (cmt) { - fprintf(out, "\n%s\nteardown {\n", cmt); + fformat(out, "\n%s\nteardown {\n", cmt); } else { - fprintf(out, "\nteardown {\n"); + fformat(out, "\nteardown {\n"); } print_step(out, spec->teardown, false); - fprintf(out, "}\n"); + fformat(out, "}\n"); } for (TestStep *s = spec->steps; s; s = s->next) @@ -126,14 +127,14 @@ cli_indent(int argc, char **argv) const char *cmt = lookup_comment(cmap, s->name); if (cmt) { - fprintf(out, "\n%s\nstep %s {\n", cmt, s->name); + fformat(out, "\n%s\nstep %s {\n", cmt, s->name); } else { - fprintf(out, "\nstep %s {\n", s->name); + fformat(out, "\nstep %s {\n", s->name); } print_step(out, s, true); - fprintf(out, "}\n"); + fformat(out, "}\n"); } /* @@ -162,10 +163,10 @@ cli_indent(int argc, char **argv) if (!redundant) { - fprintf(out, "\nsequence\n"); + fformat(out, "\nsequence\n"); for (int i = 0; i < spec->sequenceLength; i++) { - fprintf(out, " %s\n", spec->sequence[i]); + fformat(out, " %s\n", spec->sequence[i]); } } } @@ -183,10 +184,10 @@ cli_indent(int argc, char **argv) static void print_header(FILE *out, const char *path) { - FILE *f = fopen(path, "r"); + FILE *f = fopen(path, "r"); /* IGNORE-BANNED */ if (!f) { - fprintf(out, "# %s\n\n", path); + fformat(out, "# %s\n\n", path); return; } @@ -228,10 +229,10 @@ print_header(FILE *out, const char *path) if (!any) { - fprintf(out, "# %s\n", path); + fformat(out, "# %s\n", path); } - fprintf(out, "\n"); + fformat(out, "\n"); } @@ -248,7 +249,7 @@ collect_comments(const char *path) return m; } - FILE *f = fopen(path, "r"); + FILE *f = fopen(path, "r"); /* IGNORE-BANNED */ if (!f) { return m; @@ -289,7 +290,7 @@ collect_comments(const char *path) /* Check for a block keyword */ char kw[128] = ""; - if (sscanf(line, "step %127s", kw) == 1) + if (sscanf(line, "step %127s", kw) == 1) /* IGNORE-BANNED */ { /* strip trailing " {" from name */ char *brace = strchr(kw, '{'); @@ -327,7 +328,7 @@ collect_comments(const char *path) if (len > 0) { char clean[8192]; - snprintf(clean, sizeof(clean), "%.*s\n", len, start); + sformat(clean, sizeof(clean), "%.*s\n", len, start); strlcpy(m->entries[m->count].name, kw, sizeof(m->entries[0].name)); m->entries[m->count].text = strdup(clean); @@ -407,7 +408,7 @@ print_node(FILE *out, const TestNode *n, int baseIndent) { if (n->group != 0) { - snprintf(kindbuf, sizeof(kindbuf), "worker group %d", n->group); + sformat(kindbuf, sizeof(kindbuf), "worker group %d", n->group); } else { @@ -438,7 +439,7 @@ print_node(FILE *out, const TestNode *n, int baseIndent) } if (n->candidatePriority != 50) { - snprintf(numbuf, sizeof(numbuf), "%d", n->candidatePriority); + sformat(numbuf, sizeof(numbuf), "%d", n->candidatePriority); ADD("candidate-priority", numbuf); } if (!n->replicationQuorum) @@ -455,7 +456,7 @@ print_node(FILE *out, const TestNode *n, int baseIndent) } if (n->pgPort != 0) { - snprintf(numbuf, sizeof(numbuf), "%d", n->pgPort); + sformat(numbuf, sizeof(numbuf), "%d", n->pgPort); ADD("port", numbuf); } if (n->ssl[0]) @@ -495,39 +496,39 @@ print_node(FILE *out, const TestNode *n, int baseIndent) if ((lineLen <= 72 || pc == 0) && n->volumeCount == 0) { /* fits on one line — flat syntax, no "node" keyword needed */ - fprintf(out, "%*s%s", baseIndent, "", n->name); + fformat(out, "%*s%s", baseIndent, "", n->name); if (kind[0]) { - fprintf(out, " %s", kind); + fformat(out, " %s", kind); } - fprintf(out, "%s\n", inline_props); + fformat(out, "%s\n", inline_props); return; } /* multi-line — use block syntax: node { \n kind\n opt\n opt\n } */ int innerIndent = baseIndent + 4; - fprintf(out, "%*snode %s {\n", baseIndent, "", n->name); + fformat(out, "%*snode %s {\n", baseIndent, "", n->name); if (kind[0]) { - fprintf(out, "%*s%s\n", innerIndent, "", kind); + fformat(out, "%*s%s\n", innerIndent, "", kind); } for (int i = 0; i < pc; i++) { - fprintf(out, "%*s%s", innerIndent, "", props[i].kw); + fformat(out, "%*s%s", innerIndent, "", props[i].kw); if (props[i].val[0]) { - fprintf(out, " %s", props[i].val); + fformat(out, " %s", props[i].val); } - fprintf(out, "\n"); + fformat(out, "\n"); } for (int vi = 0; vi < n->volumeCount; vi++) { - fprintf(out, "%*svolume %s \"%s\"\n", + fformat(out, "%*svolume %s \"%s\"\n", innerIndent, "", n->volumes[vi].name, n->volumes[vi].path); } - fprintf(out, "%*s}\n", baseIndent, ""); + fformat(out, "%*s}\n", baseIndent, ""); } @@ -536,33 +537,33 @@ print_cluster(FILE *out, const TestSpec *spec) { const TestCluster *cl = &spec->cluster; - fprintf(out, "cluster {\n"); + fformat(out, "cluster {\n"); if (cl->withMonitor) { - fprintf(out, " monitor\n"); + fformat(out, " monitor\n"); } if (cl->secondMonitorName[0]) { - fprintf(out, " monitor %s initially stopped\n", cl->secondMonitorName); + fformat(out, " monitor %s initially stopped\n", cl->secondMonitorName); } if (cl->image[0]) { - fprintf(out, " image \"%s\"\n", cl->image); + fformat(out, " image \"%s\"\n", cl->image); } if (cl->ssl[0] && strcmp(cl->ssl, "self-signed") != 0) { - fprintf(out, " ssl %s\n", cl->ssl); + fformat(out, " ssl %s\n", cl->ssl); } if (cl->auth[0] && strcmp(cl->auth, "trust") != 0) { - fprintf(out, " auth %s\n", cl->auth); + fformat(out, " auth %s\n", cl->auth); } if (cl->extensionVersion[0]) { - fprintf(out, " extension-version %s\n", cl->extensionVersion); + fformat(out, " extension-version %s\n", cl->extensionVersion); } for (int fi = 0; fi < cl->formationCount; fi++) @@ -571,11 +572,11 @@ print_cluster(FILE *out, const TestSpec *spec) if (strcmp(f->name, "default") == 0) { - fprintf(out, " formation {\n"); + fformat(out, " formation {\n"); } else { - fprintf(out, " formation %s {\n", f->name); + fformat(out, " formation %s {\n", f->name); } for (int ni = 0; ni < f->nodeCount; ni++) @@ -583,10 +584,10 @@ print_cluster(FILE *out, const TestSpec *spec) print_node(out, &f->nodes[ni], 8); } - fprintf(out, " }\n"); + fformat(out, " }\n"); } - fprintf(out, "}\n"); + fformat(out, "}\n"); } @@ -698,7 +699,7 @@ emit_segment(FILE *out, const char *seg, int bodyIndent) { if ((int) strlen(p) <= avail) { - fprintf(out, "%*s%s\n", bodyIndent, "", p); + fformat(out, "%*s%s\n", bodyIndent, "", p); break; } @@ -714,14 +715,14 @@ emit_segment(FILE *out, const char *seg, int bodyIndent) if (last_spc) { - fprintf(out, "%*s%.*s\n", + fformat(out, "%*s%.*s\n", bodyIndent, "", (int) (last_spc - p), p); p = last_spc + 1; /* skip the space */ } else { /* No space — hard break (avoids infinite loop on long tokens). */ - fprintf(out, "%*s%.*s\n", bodyIndent, "", avail, p); + fformat(out, "%*s%.*s\n", bodyIndent, "", avail, p); p += avail; } } @@ -755,7 +756,7 @@ print_sql_body(FILE *out, const char *raw_sql, int bodyIndent) /* Short enough to fit on one line? */ if ((int) strlen(norm) <= avail) { - fprintf(out, "%*s%s\n", bodyIndent, "", norm); + fformat(out, "%*s%s\n", bodyIndent, "", norm); return; } @@ -816,13 +817,13 @@ print_cmd(FILE *out, const TestCmd *cmd, int indent) { case CMD_EXEC: { - fprintf(out, "%*sexec %s %s\n", indent, "", cmd->service, cmd->args); + fformat(out, "%*sexec %s %s\n", indent, "", cmd->service, cmd->args); break; } case CMD_EXEC_FAILS: { - fprintf(out, "%*sexec-fails %s %s\n", indent, "", + fformat(out, "%*sexec-fails %s %s\n", indent, "", cmd->service, cmd->args); break; } @@ -839,45 +840,45 @@ print_cmd(FILE *out, const TestCmd *cmd, int indent) case CMD_WAIT_STATES: { /* wait until s1, s2 [in group N] timeout Ns */ - fprintf(out, "%*swait until", indent, ""); + fformat(out, "%*swait until", indent, ""); for (int i = 0; i < cmd->waitStateCount; i++) { if (i > 0) { - fprintf(out, ","); + fformat(out, ","); } - fprintf(out, " %s", cmd->waitStates[i]); + fformat(out, " %s", cmd->waitStates[i]); } if (cmd->waitGroupCount > 0) { for (int g = 0; g < cmd->waitGroupCount; g++) { - fprintf(out, " in group %d", cmd->waitGroups[g]); + fformat(out, " in group %d", cmd->waitGroups[g]); } } - fprintf(out, " timeout %ds\n", cmd->timeoutSeconds); + fformat(out, " timeout %ds\n", cmd->timeoutSeconds); break; } case CMD_WAIT_STOPPED: { - fprintf(out, "%*swait until %s stopped timeout %ds\n", + fformat(out, "%*swait until %s stopped timeout %ds\n", indent, "", cmd->service, cmd->timeoutSeconds); break; } case CMD_PROMOTE: { - fprintf(out, "%*spromote", indent, ""); + fformat(out, "%*spromote", indent, ""); for (int i = 0; i < cmd->promoteCount; i++) { if (i > 0) { - fprintf(out, ","); + fformat(out, ","); } - fprintf(out, " %s", cmd->promoteNodes[i]); + fformat(out, " %s", cmd->promoteNodes[i]); } - fprintf(out, "\n"); + fformat(out, "\n"); break; } @@ -901,14 +902,14 @@ print_cmd(FILE *out, const TestCmd *cmd, int indent) if (oneliner_len <= 79) { - fprintf(out, "%*ssql %s { %s }\n", + fformat(out, "%*ssql %s { %s }\n", indent, "", cmd->service, norm); } else { - fprintf(out, "%*ssql %s {\n", indent, "", cmd->service); + fformat(out, "%*ssql %s {\n", indent, "", cmd->service); print_sql_body(out, norm, indent + 4); - fprintf(out, "%*s}\n", indent, ""); + fformat(out, "%*s}\n", indent, ""); } break; } @@ -919,109 +920,109 @@ print_cmd(FILE *out, const TestCmd *cmd, int indent) * { r1 } { r2 } tuple syntax by expand_tuple_expect — reverse it. */ if (strchr(cmd->expected, '\n')) { - fprintf(out, "%*sexpect {", indent, ""); + fformat(out, "%*sexpect {", indent, ""); const char *p = cmd->expected; while (*p) { const char *nl = strchr(p, '\n'); int len = nl ? (int) (nl - p) : (int) strlen(p); - fprintf(out, " { %.*s }", len, p); + fformat(out, " { %.*s }", len, p); p += len; if (*p == '\n') { p++; } } - fprintf(out, " }\n"); + fformat(out, " }\n"); } else { - fprintf(out, "%*sexpect { %s }\n", indent, "", cmd->expected); + fformat(out, "%*sexpect { %s }\n", indent, "", cmd->expected); } break; } case CMD_EXPECT_ERROR: { - fprintf(out, "%*sexpect error %s\n", indent, "", cmd->state); + fformat(out, "%*sexpect error %s\n", indent, "", cmd->state); break; } case CMD_NETWORK_OFF: { - fprintf(out, "%*snetwork disconnect %s\n", indent, "", cmd->service); + fformat(out, "%*snetwork disconnect %s\n", indent, "", cmd->service); break; } case CMD_NETWORK_ON: { - fprintf(out, "%*snetwork connect %s\n", indent, "", cmd->service); + fformat(out, "%*snetwork connect %s\n", indent, "", cmd->service); break; } case CMD_SLEEP: { - fprintf(out, "%*ssleep %ds\n", indent, "", cmd->timeoutSeconds); + fformat(out, "%*ssleep %ds\n", indent, "", cmd->timeoutSeconds); break; } case CMD_COMPOSE_DOWN: { - fprintf(out, "%*scompose down\n", indent, ""); + fformat(out, "%*scompose down\n", indent, ""); break; } case CMD_COMPOSE_START: { - fprintf(out, "%*scompose start %s\n", indent, "", cmd->service); + fformat(out, "%*scompose start %s\n", indent, "", cmd->service); break; } case CMD_COMPOSE_STOP: { - fprintf(out, "%*scompose stop %s\n", indent, "", cmd->service); + fformat(out, "%*scompose stop %s\n", indent, "", cmd->service); break; } case CMD_COMPOSE_KILL: { - fprintf(out, "%*scompose kill %s\n", indent, "", cmd->service); + fformat(out, "%*scompose kill %s\n", indent, "", cmd->service); break; } case CMD_PG_AUTOCTL: { - fprintf(out, "%*spg_autoctl %s\n", indent, "", cmd->args); + fformat(out, "%*spg_autoctl %s\n", indent, "", cmd->args); break; } case CMD_STOP_POSTGRES: { - fprintf(out, "%*sstop postgres %s\n", indent, "", cmd->service); + fformat(out, "%*sstop postgres %s\n", indent, "", cmd->service); break; } case CMD_START_POSTGRES: { - fprintf(out, "%*sstart postgres %s\n", indent, "", cmd->service); + fformat(out, "%*sstart postgres %s\n", indent, "", cmd->service); break; } case CMD_STAYS_WHILE: { - fprintf(out, "%*sassert %s stays %s while {\n", + fformat(out, "%*sassert %s stays %s while {\n", indent, "", cmd->service, cmd->state); for (const TestCmd *bc = cmd->body; bc; bc = bc->next) { print_cmd(out, bc, indent + 4); } - fprintf(out, "%*s}\n", indent, ""); + fformat(out, "%*s}\n", indent, ""); break; } case CMD_COMPOSE_INJECT: { - fprintf(out, "%*scompose inject %s %s %s:%s\n", + fformat(out, "%*scompose inject %s %s %s:%s\n", indent, "", cmd->expected, /* image */ cmd->args, /* src path */ @@ -1032,7 +1033,7 @@ print_cmd(FILE *out, const TestCmd *cmd, int indent) case CMD_SET_MONITOR: { - fprintf(out, "%*sset monitor %s\n", indent, "", cmd->service); + fformat(out, "%*sset monitor %s\n", indent, "", cmd->service); break; } @@ -1043,7 +1044,7 @@ print_cmd(FILE *out, const TestCmd *cmd, int indent) * The logsNegate flag adds "not " and allowError picks the verb. */ const char *verb = cmd->allowError ? "matches" : "contains"; - fprintf(out, "%*slogs %s %s%s \"%s\"\n", + fformat(out, "%*slogs %s %s%s \"%s\"\n", indent, "", cmd->service, cmd->logsNegate ? "not " : "", @@ -1054,7 +1055,7 @@ print_cmd(FILE *out, const TestCmd *cmd, int indent) default: { - fprintf(out, "%*s(unknown cmd %d)\n", indent, "", cmd->kind); + fformat(out, "%*s(unknown cmd %d)\n", indent, "", cmd->kind); break; } } @@ -1071,15 +1072,15 @@ print_cmd_wait(FILE *out, const TestCmd *cmd, int indent) if (cmd->kind == CMD_WAIT_MULTI) { /* first condition on same line, subsequent "and" lines indented */ - fprintf(out, "%*swait until %s state is %s\n", + fformat(out, "%*swait until %s state is %s\n", indent, "", cmd->waitNodes[0], cmd->waitStates[0]); for (int i = 1; i < cmd->waitStateCount; i++) { - fprintf(out, "%*sand %s state is %s\n", + fformat(out, "%*sand %s state is %s\n", indent + 4, "", cmd->waitNodes[i], cmd->waitStates[i]); } - fprintf(out, "%*s timeout %ds\n", indent, "", cmd->timeoutSeconds); + fformat(out, "%*s timeout %ds\n", indent, "", cmd->timeoutSeconds); return; } @@ -1091,12 +1092,12 @@ print_cmd_wait(FILE *out, const TestCmd *cmd, int indent) { if (cmd->timeoutSeconds > 0) { - fprintf(out, "%*swait until %s assigned-state = %s timeout %ds\n", + fformat(out, "%*swait until %s assigned-state = %s timeout %ds\n", indent, "", cmd->service, cmd->state, cmd->timeoutSeconds); } else { - fprintf(out, "%*sassert %s assigned-state = %s\n", + fformat(out, "%*sassert %s assigned-state = %s\n", indent, "", cmd->service, cmd->state); } return; @@ -1105,7 +1106,7 @@ print_cmd_wait(FILE *out, const TestCmd *cmd, int indent) /* CMD_ASSERT_STATE with no timeout: instant check → canonical assert form */ if (cmd->kind == CMD_ASSERT_STATE && cmd->timeoutSeconds == 0) { - fprintf(out, "%*sassert %s state = %s\n", + fformat(out, "%*sassert %s state = %s\n", indent, "", cmd->service, cmd->state); return; } @@ -1116,31 +1117,31 @@ print_cmd_wait(FILE *out, const TestCmd *cmd, int indent) if (cmd->passThroughCount > 0) { /* multi-line: target state, then "passing through" line */ - fprintf(out, "%*s%s %s state is %s\n", + fformat(out, "%*s%s %s state is %s\n", indent, "", op, cmd->service, cmd->state); - fprintf(out, "%*s passing through", indent, ""); + fformat(out, "%*s passing through", indent, ""); for (int i = 0; i < cmd->passThroughCount; i++) { if (i > 0) { - fprintf(out, ","); + fformat(out, ","); } - fprintf(out, " %s", cmd->passThroughStates[i]); + fformat(out, " %s", cmd->passThroughStates[i]); } - fprintf(out, "\n"); - fprintf(out, "%*s timeout %ds\n", indent, "", cmd->timeoutSeconds); + fformat(out, "\n"); + fformat(out, "%*s timeout %ds\n", indent, "", cmd->timeoutSeconds); return; } /* simple single-line */ if (cmd->timeoutSeconds > 0) { - fprintf(out, "%*s%s %s state is %s timeout %ds\n", + fformat(out, "%*s%s %s state is %s timeout %ds\n", indent, "", op, cmd->service, cmd->state, cmd->timeoutSeconds); } else { - fprintf(out, "%*s%s %s state is %s\n", + fformat(out, "%*s%s %s state is %s\n", indent, "", op, cmd->service, cmd->state); } } diff --git a/src/bin/pgaftest/cli_root.c b/src/bin/pgaftest/cli_root.c index acb845074..5b724cc2e 100644 --- a/src/bin/pgaftest/cli_root.c +++ b/src/bin/pgaftest/cli_root.c @@ -14,6 +14,8 @@ #include "commandline.h" #include "defaults.h" +#include "file_utils.h" +#include "string_utils.h" #include "log.h" #include "test_spec.h" #include "test_runner.h" @@ -68,13 +70,13 @@ derive_work_dir(const char *specPath, char *buf, int buflen) *dot = '\0'; } - const char *tmpdir = getenv("TMPDIR"); + const char *tmpdir = getenv("TMPDIR"); /* IGNORE-BANNED */ if (!tmpdir || *tmpdir == '\0') { tmpdir = "/tmp"; } - snprintf(buf, buflen, "%s/pgaftest/%s", tmpdir, name); + sformat(buf, buflen, "%s/pgaftest/%s", tmpdir, name); } @@ -175,7 +177,7 @@ cli_run(int argc, char **argv) /* Handle --schedule file */ if (pgaftestOpts.schedule[0] != '\0') { - FILE *f = fopen(pgaftestOpts.schedule, "r"); + FILE *f = fopen(pgaftestOpts.schedule, "r"); /* IGNORE-BANNED */ if (!f) { log_error("Cannot open schedule \"%s\": %m", @@ -219,8 +221,8 @@ cli_run(int argc, char **argv) } else { - snprintf(specPath, sizeof(specPath), - "tests/tap/specs/%s.pgaf", p); + sformat(specPath, sizeof(specPath), + "tests/tap/specs/%s.pgaf", p); } char workDir[1024]; @@ -236,8 +238,8 @@ cli_run(int argc, char **argv) { *dot = '\0'; } - snprintf(workDir, sizeof(workDir), - "%s/%s", pgaftestOpts.workDir, name); + sformat(workDir, sizeof(workDir), + "%s/%s", pgaftestOpts.workDir, name); } else { @@ -260,7 +262,7 @@ cli_run(int argc, char **argv) } fclose(f); - fprintf(stderr, "\nSchedule complete: %d/%d passed\n", + fformat(stderr, "\nSchedule complete: %d/%d passed\n", total - failed, total); exit(failed > 0 ? 1 : 0); } @@ -353,8 +355,8 @@ cli_step(int argc, char **argv) /* We need the spec file too — look for it in workDir */ char specPath[1024]; - snprintf(specPath, sizeof(specPath), "%s/spec.pgaf", - pgaftestOpts.workDir); + sformat(specPath, sizeof(specPath), "%s/spec.pgaf", + pgaftestOpts.workDir); /* If there's a second positional arg, treat it as the spec path */ if (argc >= 2 && argv[1] != NULL) @@ -463,8 +465,8 @@ cli_down(int argc, char **argv) } else { - snprintf(specPath, sizeof(specPath), "%s/spec.pgaf", - pgaftestOpts.workDir); + sformat(specPath, sizeof(specPath), "%s/spec.pgaf", + pgaftestOpts.workDir); } TestSpec *spec = NULL; @@ -488,10 +490,10 @@ cli_down(int argc, char **argv) } char cmd[2048]; - snprintf(cmd, sizeof(cmd), - "docker compose -p %s -f %s/docker-compose.yml " - "down --volumes --remove-orphans", - projectName, pgaftestOpts.workDir); + sformat(cmd, sizeof(cmd), + "docker compose -p %s -f %s/docker-compose.yml " + "down --volumes --remove-orphans", + projectName, pgaftestOpts.workDir); int rc = system(cmd); exit(rc == 0 ? 0 : 1); diff --git a/src/bin/pgaftest/compose_gen.c b/src/bin/pgaftest/compose_gen.c index f55e9f488..e37a5b4b4 100644 --- a/src/bin/pgaftest/compose_gen.c +++ b/src/bin/pgaftest/compose_gen.c @@ -62,7 +62,7 @@ static const char * debian_pg_version(void) { - const char *v = getenv("PGVERSION"); + const char *v = getenv("PGVERSION"); /* IGNORE-BANNED */ return (v && v[0]) ? v : "17"; } @@ -121,7 +121,7 @@ run_openssl(const char *const *argv) for (int i = 1; argv[i]; i++) { - pos += snprintf(cmd + pos, sizeof(cmd) - pos, " %s", argv[i]); + pos += sformat(cmd + pos, sizeof(cmd) - pos, " %s", argv[i]); if (pos >= (int) sizeof(cmd) - 1) { log_error("openssl command too long"); @@ -355,15 +355,15 @@ write_image_stanza_target(FILE *f, const TestCluster *cluster, bool isTestRunner = (strcmp(target, "pgaftest") == 0); /* cluster-level image overrides PGAF_IMAGE env var */ - const char *img = cluster->image[0] ? cluster->image : getenv("PGAF_IMAGE"); + const char *img = cluster->image[0] ? cluster->image : getenv("PGAF_IMAGE"); /* IGNORE-BANNED */ if (img && *img && !isTestRunner) { - fprintf(f, " image: \"%s\"\n", img); + fformat(f, " image: \"%s\"\n", img); } else { - fprintf(f, + fformat(f, " build:\n" " context: \"%s\"\n" " target: %s\n", @@ -464,16 +464,16 @@ compose_gen_write(TestCluster *cluster, const char *contextDir, const char *specFile) { - FILE *f = fopen(path, "w"); + FILE *f = fopen(path, "w"); /* IGNORE-BANNED */ if (!f) { log_error("Failed to open \"%s\" for writing: %m", path); return false; } - fprintf(f, "# Generated by pgaftest — do not edit manually\n"); - fprintf(f, "# Project: %s\n\n", projectName); - fprintf(f, "services:\n"); + fformat(f, "# Generated by pgaftest — do not edit manually\n"); + fformat(f, "# Project: %s\n\n", projectName); + fformat(f, "services:\n"); /* ---- monitor (optional) ---- */ if (cluster->withMonitor) @@ -484,7 +484,7 @@ compose_gen_write(TestCluster *cluster, cluster->monitorHostPort = pick_free_port(); } - fprintf(f, " monitor:\n"); + fformat(f, " monitor:\n"); if (cluster->monitorDebianCluster[0]) { write_image_stanza_target(f, cluster, contextDir, "debian"); @@ -502,23 +502,23 @@ compose_gen_write(TestCluster *cluster, char monitor_pgdata[MAXPGPATH]; if (cluster->monitorDebianCluster[0]) { - snprintf(monitor_pgdata, sizeof(monitor_pgdata), - DEBIAN_PGDATA_PREFIX "/%s/%s", - debian_pg_version(), cluster->monitorDebianCluster); + sformat(monitor_pgdata, sizeof(monitor_pgdata), + DEBIAN_PGDATA_PREFIX "/%s/%s", + debian_pg_version(), cluster->monitorDebianCluster); } else { strlcpy(monitor_pgdata, NODE_PGDATA, sizeof(monitor_pgdata)); } - fprintf(f, + fformat(f, " hostname: monitor\n" " volumes:\n" " - monitor_data:/var/lib/postgres:rw\n" " - ./monitor.ini:" NODE_INI_PATH ":ro\n"); if (ssl_needs_certs(cluster->ssl)) { - fprintf(f, + fformat(f, " - ./ssl/ca.crt:" SSL_DIR_IN_CONTAINER "/ca.crt:ro\n" " - ./ssl/monitor:" SSL_DIR_IN_CONTAINER "/server:ro\n" @@ -527,25 +527,25 @@ compose_gen_write(TestCluster *cluster, } if (cluster->bindSource) { - fprintf(f, + fformat(f, " - %s:/usr/src/pg_auto_failover:rw\n", contextDir); } - fprintf(f, + fformat(f, " environment:\n" " PGDATA: %s\n", monitor_pgdata); if (cluster->extensionVersion[0]) { - fprintf(f, + fformat(f, " PG_AUTOCTL_EXTENSION_VERSION: \"%s\"\n", cluster->extensionVersion); } - fprintf(f, + fformat(f, " ports:\n" " - \"%d:5432\"\n", cluster->monitorHostPort); - fprintf(f, + fformat(f, " command: [\"/bin/sh\", \"-c\"," " \"%s rm -f /tmp/pg_autoctl%s/pg_autoctl.pid" " && exec pg_autoctl node run " NODE_INI_PATH "\"]\n" @@ -563,9 +563,9 @@ compose_gen_write(TestCluster *cluster, } const char *svc = cluster->secondMonitorName; - fprintf(f, " %s:\n", svc); + fformat(f, " %s:\n", svc); write_image_stanza(f, cluster, contextDir); - fprintf(f, + fformat(f, " hostname: %s\n" " volumes:\n" " - %s_data:/var/lib/postgres:rw\n" @@ -573,7 +573,7 @@ compose_gen_write(TestCluster *cluster, svc, svc, svc); if (ssl_needs_certs(cluster->ssl)) { - fprintf(f, + fformat(f, " - ./ssl/ca.crt:" SSL_DIR_IN_CONTAINER "/ca.crt:ro\n" " - ./ssl/%s:" SSL_DIR_IN_CONTAINER "/server:ro\n" @@ -583,17 +583,17 @@ compose_gen_write(TestCluster *cluster, } if (cluster->bindSource) { - fprintf(f, + fformat(f, " - %s:/usr/src/pg_auto_failover:rw\n", contextDir); } - fprintf(f, + fformat(f, " environment:\n" " PGDATA: " NODE_PGDATA "\n"); - fprintf(f, + fformat(f, " ports:\n" " - \"%d:5432\"\n", cluster->secondMonitorHostPort); - fprintf(f, + fformat(f, " command: [\"/bin/sh\", \"-c\"," " \"%s rm -f /tmp/pg_autoctl" NODE_PGDATA "/pg_autoctl.pid" " && exec pg_autoctl node run " @@ -619,7 +619,7 @@ compose_gen_write(TestCluster *cluster, for (int ni = 0; ni < form->nodeCount; ni++) { const TestNode *n = &form->nodes[ni]; - fprintf(f, " %s:\n", n->name); + fformat(f, " %s:\n", n->name); if (n->debianCluster[0]) { write_image_stanza_target(f, cluster, contextDir, "debian"); @@ -633,16 +633,16 @@ compose_gen_write(TestCluster *cluster, char node_pgdata[MAXPGPATH]; if (n->debianCluster[0]) { - snprintf(node_pgdata, sizeof(node_pgdata), - DEBIAN_PGDATA_PREFIX "/%s/%s", - debian_pg_version(), n->debianCluster); + sformat(node_pgdata, sizeof(node_pgdata), + DEBIAN_PGDATA_PREFIX "/%s/%s", + debian_pg_version(), n->debianCluster); } else { strlcpy(node_pgdata, NODE_PGDATA, sizeof(node_pgdata)); } - fprintf(f, + fformat(f, " hostname: %s\n" " volumes:\n" " - %s_data:/var/lib/postgres:rw\n" @@ -653,7 +653,7 @@ compose_gen_write(TestCluster *cluster, n->launchDeferred ? "rw" : "ro"); if (ssl_needs_certs(cluster->ssl)) { - fprintf(f, + fformat(f, " - ./ssl/ca.crt:" SSL_DIR_IN_CONTAINER "/ca.crt:ro\n" " - ./ssl/%s:" SSL_DIR_IN_CONTAINER "/server:ro\n" @@ -663,10 +663,10 @@ compose_gen_write(TestCluster *cluster, } for (int vi = 0; vi < n->volumeCount; vi++) { - fprintf(f, " - %s_%s:%s:rw\n", + fformat(f, " - %s_%s:%s:rw\n", n->volumes[vi].name, n->name, n->volumes[vi].path); } - fprintf(f, + fformat(f, " environment:\n" " PGDATA: %s\n" " PGUSER: demo\n" @@ -684,7 +684,7 @@ compose_gen_write(TestCluster *cluster, /* per-node ssl override may differ from cluster default */ const char *node_ssl = n->ssl[0] ? n->ssl : cluster->ssl; - fprintf(f, + fformat(f, " command: [\"/bin/sh\", \"-c\"," " \"%s rm -f /tmp/pg_autoctl%s/pg_autoctl.pid" " && exec pg_autoctl node run " NODE_INI_PATH "\"]\n" @@ -707,7 +707,7 @@ compose_gen_write(TestCluster *cluster, */ if (!firstNode && cluster->withMonitor) { - fprintf(f, + fformat(f, " healthcheck:\n" " test: [\"CMD\", \"pg_autoctl\", \"status\"," " \"--pgdata\", \"%s\"]\n" @@ -720,14 +720,14 @@ compose_gen_write(TestCluster *cluster, if (firstNode) { - fprintf(f, + fformat(f, " depends_on:\n" " %s:\n" " condition: %s\n", firstNode->name, cluster->withMonitor ? "service_healthy" : "service_started"); } - fprintf(f, "\n"); + fformat(f, "\n"); if (!firstNode) { @@ -751,7 +751,7 @@ compose_gen_write(TestCluster *cluster, { /* Determine which service pgaftest must wait for before starting. */ /* It needs the cluster fully ready: monitor healthy + all nodes started. */ - fprintf(f, " pgaftest:\n"); + fformat(f, " pgaftest:\n"); write_image_stanza_target(f, cluster, contextDir, "pgaftest"); /* Build monitor pguri for PG_AUTOCTL_MONITOR */ @@ -786,7 +786,7 @@ compose_gen_write(TestCluster *cluster, strlcpy(workDir, ".", sizeof(workDir)); } - fprintf(f, + fformat(f, " user: root\n" " volumes:\n" " - %s:/spec.pgaf:ro\n" @@ -795,13 +795,13 @@ compose_gen_write(TestCluster *cluster, specFile, workDir, workDir); if (ssl_needs_certs(cluster->ssl)) { - fprintf(f, + fformat(f, " - %s/ssl/ca.crt:/root/.postgresql/root.crt:ro\n" " - %s/ssl/client/postgresql.crt:/root/.postgresql/postgresql.crt:ro\n" " - %s/ssl/client/postgresql.key:/root/.postgresql/postgresql.key:ro\n", workDir, workDir, workDir); } - fprintf(f, + fformat(f, " environment:\n" " PGAFTEST_COMPOSE_SERVICE: \"1\"\n" " PGAFTEST_HOST_WORK_DIR: \"%s\"\n" @@ -809,20 +809,20 @@ compose_gen_write(TestCluster *cluster, workDir, projectName); if (cluster->withMonitor) { - fprintf(f, " PG_AUTOCTL_MONITOR: \"%s\"\n", monitorPguri); + fformat(f, " PG_AUTOCTL_MONITOR: \"%s\"\n", monitorPguri); } - fprintf(f, " command: [\"pgaftest\", \"run\", \"/spec.pgaf\"]\n\n"); + fformat(f, " command: [\"pgaftest\", \"run\", \"/spec.pgaf\"]\n\n"); } /* ---- volumes ---- */ - fprintf(f, "volumes:\n"); + fformat(f, "volumes:\n"); if (cluster->withMonitor) { - fprintf(f, " monitor_data:\n"); + fformat(f, " monitor_data:\n"); } if (cluster->secondMonitorName[0]) { - fprintf(f, " %s_data:\n", cluster->secondMonitorName); + fformat(f, " %s_data:\n", cluster->secondMonitorName); } for (int fi = 0; fi < cluster->formationCount; fi++) { @@ -830,10 +830,10 @@ compose_gen_write(TestCluster *cluster, for (int ni = 0; ni < form->nodeCount; ni++) { const TestNode *n = &form->nodes[ni]; - fprintf(f, " %s_data:\n", n->name); + fformat(f, " %s_data:\n", n->name); for (int vi = 0; vi < n->volumeCount; vi++) { - fprintf(f, " %s_%s:\n", n->volumes[vi].name, n->name); + fformat(f, " %s_%s:\n", n->volumes[vi].name, n->name); } } } @@ -858,7 +858,7 @@ compose_gen_write_monitor_ini(const TestCluster *cluster, const char *dir) char path[1024]; sformat(path, sizeof(path), "%s/monitor.ini", dir); - FILE *f = fopen(path, "w"); + FILE *f = fopen(path, "w"); /* IGNORE-BANNED */ if (!f) { log_error("Failed to open \"%s\" for writing: %m", path); @@ -868,16 +868,16 @@ compose_gen_write_monitor_ini(const TestCluster *cluster, const char *dir) char mon_pgdata[MAXPGPATH]; if (cluster->monitorDebianCluster[0]) { - snprintf(mon_pgdata, sizeof(mon_pgdata), - DEBIAN_PGDATA_PREFIX "/%s/%s", - debian_pg_version(), cluster->monitorDebianCluster); + sformat(mon_pgdata, sizeof(mon_pgdata), + DEBIAN_PGDATA_PREFIX "/%s/%s", + debian_pg_version(), cluster->monitorDebianCluster); } else { strlcpy(mon_pgdata, NODE_PGDATA, sizeof(mon_pgdata)); } - fprintf(f, + fformat(f, "# Generated by pgaftest — do not edit manually\n" "# Monitor node configuration\n" "\n" @@ -899,7 +899,7 @@ compose_gen_write_monitor_ini(const TestCluster *cluster, const char *dir) if (ssl_needs_certs(cluster->ssl)) { - fprintf(f, + fformat(f, "\n" "[ssl]\n" "ca_file = " SSL_DIR_IN_CONTAINER "/ca.crt\n" @@ -916,7 +916,7 @@ compose_gen_write_monitor_ini(const TestCluster *cluster, const char *dir) if (cluster->monitorPassword[0]) { - fprintf(f, + fformat(f, "\n" "[pg_auto_failover]\n" "autoctl_node_password = %s\n", @@ -933,7 +933,7 @@ compose_gen_write_monitor_ini(const TestCluster *cluster, const char *dir) } /* kind defaults to "ha" — the monitor uses that default when absent */ - fprintf(f, "\n[formation %s]\n", form->name); + fformat(f, "\n[formation %s]\n", form->name); } fclose(f); @@ -960,14 +960,14 @@ compose_gen_write_second_monitor_ini(const TestCluster *cluster, const char *dir char path[1024]; sformat(path, sizeof(path), "%s/%s.ini", dir, name); - FILE *f = fopen(path, "w"); + FILE *f = fopen(path, "w"); /* IGNORE-BANNED */ if (!f) { log_error("Failed to open \"%s\" for writing: %m", path); return false; } - fprintf(f, + fformat(f, "# Generated by pgaftest — do not edit manually\n" "# Second monitor (replacement): %s\n" "\n" @@ -989,7 +989,7 @@ compose_gen_write_second_monitor_ini(const TestCluster *cluster, const char *dir if (ssl_needs_certs(cluster->ssl)) { - fprintf(f, + fformat(f, "\n" "[ssl]\n" "ca_file = " SSL_DIR_IN_CONTAINER "/ca.crt\n" @@ -1024,7 +1024,7 @@ compose_gen_write_node_ini(const TestCluster *cluster, char path[1024]; sformat(path, sizeof(path), "%s/%s.ini", dir, node->name); - FILE *f = fopen(path, "w"); + FILE *f = fopen(path, "w"); /* IGNORE-BANNED */ if (!f) { log_error("Failed to open \"%s\" for writing: %m", path); @@ -1059,7 +1059,7 @@ compose_gen_write_node_ini(const TestCluster *cluster, int pg_port = node->pgPort > 0 ? node->pgPort : 5432; - fprintf(f, + fformat(f, "# Generated by pgaftest — do not edit manually\n" "# Node: %s (formation: %s)\n" "\n" @@ -1076,12 +1076,12 @@ compose_gen_write_node_ini(const TestCluster *cluster, if (node->debianCluster[0]) { - fprintf(f, "debian_cluster = %s\n", node->debianCluster); + fformat(f, "debian_cluster = %s\n", node->debianCluster); } if (node->debianCluster[0]) { - fprintf(f, + fformat(f, "\n" "[postgresql]\n" "pgdata = " DEBIAN_PGDATA_PREFIX "/%s/%s\n" @@ -1090,7 +1090,7 @@ compose_gen_write_node_ini(const TestCluster *cluster, } else { - fprintf(f, + fformat(f, "\n" "[postgresql]\n" "pgdata = " NODE_PGDATA "\n" @@ -1099,16 +1099,16 @@ compose_gen_write_node_ini(const TestCluster *cluster, if (node->noMonitor) { - fprintf(f, "[monitor]\nno_monitor = true\n"); + fformat(f, "[monitor]\nno_monitor = true\n"); if (nodeId > 0) { - fprintf(f, "node_id = %d\n", nodeId); + fformat(f, "node_id = %d\n", nodeId); } - fprintf(f, "\n"); + fformat(f, "\n"); } else if (cluster->monitorPassword[0]) { - fprintf(f, + fformat(f, "[monitor]\n" "pguri = postgresql://autoctl_node:%s@monitor/pg_auto_failover\n" "\n", @@ -1116,13 +1116,13 @@ compose_gen_write_node_ini(const TestCluster *cluster, } else { - fprintf(f, + fformat(f, "[monitor]\n" "pguri = " MONITOR_PGURI "\n" "\n"); } - fprintf(f, + fformat(f, "[formation]\n" "name = %s\n" "group = %d\n" @@ -1138,18 +1138,18 @@ compose_gen_write_node_ini(const TestCluster *cluster, /* Citus role/cluster settings live in their own [citus] section */ if (node->citusSecondary || node->citusClusterName[0]) { - fprintf(f, "\n[citus]\n"); + fformat(f, "\n[citus]\n"); if (node->citusSecondary) { - fprintf(f, "role = secondary\n"); + fformat(f, "role = secondary\n"); } if (node->citusClusterName[0]) { - fprintf(f, "cluster_name = %s\n", node->citusClusterName); + fformat(f, "cluster_name = %s\n", node->citusClusterName); } } - fprintf(f, + fformat(f, "\n" "[options]\n" "ssl = %s\n" @@ -1161,7 +1161,7 @@ compose_gen_write_node_ini(const TestCluster *cluster, if (ssl_needs_certs(eff_ssl)) { - fprintf(f, + fformat(f, "\n" "[ssl]\n" "ca_file = " SSL_DIR_IN_CONTAINER "/ca.crt\n" @@ -1178,11 +1178,11 @@ compose_gen_write_node_ini(const TestCluster *cluster, if (node->listen) { - fprintf(f, "listen = 0.0.0.0\n"); + fformat(f, "listen = 0.0.0.0\n"); } if (node->launchDeferred) { - fprintf(f, "\n[launch]\nmode = deferred\n"); + fformat(f, "\n[launch]\nmode = deferred\n"); } /* @@ -1210,7 +1210,7 @@ compose_gen_write_node_ini(const TestCluster *cluster, */ if (eff_replication_pw[0]) { - fprintf(f, + fformat(f, "\n" "[replication]\n" "password = %s\n", @@ -1219,7 +1219,7 @@ compose_gen_write_node_ini(const TestCluster *cluster, if (eff_monitor_pw[0]) { - fprintf(f, + fformat(f, "\n" "[pg_auto_failover]\n" "monitor_password = %s\n", @@ -1240,7 +1240,7 @@ compose_gen_write_node_ini(const TestCluster *cluster, void compose_network_name(const char *projectName, char *buf, int buflen) { - snprintf(buf, buflen, "%s_default", projectName); + sformat(buf, buflen, "%s_default", projectName); } @@ -1248,5 +1248,5 @@ void compose_container_name(const char *projectName, const char *service, char *buf, int buflen) { - snprintf(buf, buflen, "%s-%s-1", projectName, service); + sformat(buf, buflen, "%s-%s-1", projectName, service); } diff --git a/src/bin/pgaftest/test_runner.c b/src/bin/pgaftest/test_runner.c index ab29dea1f..c0f528c34 100644 --- a/src/bin/pgaftest/test_runner.c +++ b/src/bin/pgaftest/test_runner.c @@ -15,6 +15,8 @@ #include #include +#include "file_utils.h" +#include "string_utils.h" #include "log.h" #include "compose_gen.h" @@ -52,7 +54,7 @@ run_cmd(const char *fmt, ...) char cmd[4096]; va_list ap; va_start(ap, fmt); - vsnprintf(cmd, sizeof(cmd), fmt, ap); + pg_vsnprintf(cmd, sizeof(cmd), fmt, ap); va_end(ap); log_debug("$ %s", cmd); @@ -67,7 +69,7 @@ run_cmd_capture(char *buf, int buflen, const char *fmt, ...) char cmd[4096]; va_list ap; va_start(ap, fmt); - vsnprintf(cmd, sizeof(cmd), fmt, ap); + pg_vsnprintf(cmd, sizeof(cmd), fmt, ap); va_end(ap); log_debug("$ %s", cmd); @@ -111,7 +113,7 @@ runner_init(TestRunner *r, TestSpec *spec, const char *workDir) * authoritative (the container is invoked with /spec.pgaf which would * otherwise yield "spec"). On the host, derive from the spec filename. */ - const char *envProject = getenv("COMPOSE_PROJECT_NAME"); + const char *envProject = getenv("COMPOSE_PROJECT_NAME"); /* IGNORE-BANNED */ if (envProject && *envProject) { strlcpy(r->projectName, envProject, sizeof(r->projectName)); @@ -128,16 +130,16 @@ runner_init(TestRunner *r, TestSpec *spec, const char *workDir) } } - snprintf(r->workDir, sizeof(r->workDir), "%s", workDir); - snprintf(r->composeFile, sizeof(r->composeFile), - "%s/docker-compose.yml", workDir); + sformat(r->workDir, sizeof(r->workDir), "%s", workDir); + sformat(r->composeFile, sizeof(r->composeFile), + "%s/docker-compose.yml", workDir); /* * Inside the compose network the compose file lives on the HOST filesystem * and is not accessible from the container. Docker Compose v2 can exec * into a running project by project-name alone; omit -f in that case. */ - if (getenv("PGAFTEST_COMPOSE_SERVICE")) + if (getenv("PGAFTEST_COMPOSE_SERVICE")) /* IGNORE-BANNED */ { /* * Inside the pgaftest container, the compose file lives on the HOST @@ -145,23 +147,23 @@ runner_init(TestRunner *r, TestSpec *spec, const char *workDir) * container so Docker daemon can find it). Use -f to point Docker * Compose at it so that `up -d` can create new containers. */ - const char *hostWorkDir = getenv("PGAFTEST_HOST_WORK_DIR"); + const char *hostWorkDir = getenv("PGAFTEST_HOST_WORK_DIR"); /* IGNORE-BANNED */ if (hostWorkDir && hostWorkDir[0]) { - snprintf(r->composeBase, sizeof(r->composeBase), - "docker compose -p %s -f %s/docker-compose.yml", - r->projectName, hostWorkDir); + sformat(r->composeBase, sizeof(r->composeBase), + "docker compose -p %s -f %s/docker-compose.yml", + r->projectName, hostWorkDir); } else { - snprintf(r->composeBase, sizeof(r->composeBase), - "docker compose -p %s", r->projectName); + sformat(r->composeBase, sizeof(r->composeBase), + "docker compose -p %s", r->projectName); } } else { - snprintf(r->composeBase, sizeof(r->composeBase), - "docker compose -p %s -f %s", r->projectName, r->composeFile); + sformat(r->composeBase, sizeof(r->composeBase), + "docker compose -p %s -f %s", r->projectName, r->composeFile); } /* build context = directory from which pgaftest was invoked */ @@ -178,8 +180,8 @@ runner_init(TestRunner *r, TestSpec *spec, const char *workDir) } else { - snprintf(r->specFile, sizeof(r->specFile), "%s/%s", - r->contextDir, spec->filename); + sformat(r->specFile, sizeof(r->specFile), "%s/%s", + r->contextDir, spec->filename); } /* Start with the primary monitor as the active target. */ @@ -198,9 +200,9 @@ tap_buf(TestRunner *r, const char *fmt, ...) { va_list ap; va_start(ap, fmt); - int n = vsnprintf(r->tapBuffer + r->tapBufferLen, - sizeof(r->tapBuffer) - r->tapBufferLen, - fmt, ap); + int n = pg_vsnprintf(r->tapBuffer + r->tapBufferLen, + sizeof(r->tapBuffer) - r->tapBufferLen, + fmt, ap); va_end(ap); if (n > 0) { @@ -213,7 +215,7 @@ tap_buf(TestRunner *r, const char *fmt, ...) static void tap_plan(TestRunner *r) { - printf("1..%d\n", r->tapTotal); + fformat(stdout, "1..%d\n", r->tapTotal); if (r->tapBufferLen > 0) { fwrite(r->tapBuffer, 1, r->tapBufferLen, stdout); @@ -248,9 +250,9 @@ tap_diag(const char *fmt, ...) char buf[1024]; va_list ap; va_start(ap, fmt); - vsnprintf(buf, sizeof(buf), fmt, ap); + pg_vsnprintf(buf, sizeof(buf), fmt, ap); va_end(ap); - printf("# %s\n", buf); + fformat(stdout, "# %s\n", buf); fflush(stdout); } @@ -293,7 +295,7 @@ runner_compose_generate(TestRunner *r) * makes sense for the in-compose CI mode where `docker compose up * --exit-code-from pgaftest` drives the whole run. Pass NULL to omit it. */ - const char *specFileForCompose = getenv("PGAFTEST_COMPOSE_SERVICE") + const char *specFileForCompose = getenv("PGAFTEST_COMPOSE_SERVICE") /* IGNORE-BANNED */ ? r->specFile : NULL; if (!compose_gen_write(&r->spec->cluster, @@ -439,7 +441,7 @@ runner_compose_down(TestRunner *r) * "compose down" sends SIGKILL to our own container, so skip it — the host * runner already calls "compose down" after we exit. */ - if (getenv("PGAFTEST_COMPOSE_SERVICE")) + if (getenv("PGAFTEST_COMPOSE_SERVICE")) /* IGNORE-BANNED */ { r->composeUp = false; return true; @@ -545,9 +547,9 @@ static bool get_node_pgdata(TestRunner *r, const char *nodeName, char *pgdata, int len) { char iniPath[1280]; - snprintf(iniPath, sizeof(iniPath), "%s/%s.ini", r->workDir, nodeName); + sformat(iniPath, sizeof(iniPath), "%s/%s.ini", r->workDir, nodeName); - FILE *f = fopen(iniPath, "r"); + FILE *f = fopen(iniPath, "r"); /* IGNORE-BANNED */ if (!f) { return false; @@ -629,7 +631,10 @@ monitor_get_node_health(TestRunner *r, const char *nodeName, *p2 = '\0'; strlcpy(goal, p1 + 1, goallen); - *health = atoi(p2 + 1); + if (!stringToInt(p2 + 1, health)) + { + return false; + } return true; } @@ -732,7 +737,7 @@ runner_notify_connect(TestRunner *r) char connstr[512]; - if (getenv("PGAFTEST_COMPOSE_SERVICE")) + if (getenv("PGAFTEST_COMPOSE_SERVICE")) /* IGNORE-BANNED */ { /* * Inside the compose network: connect directly via the monitor service @@ -741,17 +746,17 @@ runner_notify_connect(TestRunner *r) */ if (cl->monitorPassword[0]) { - snprintf(connstr, sizeof(connstr), - "host=%s port=5432 dbname=pg_auto_failover " - "user=autoctl_node password=%s connect_timeout=5", - svc, cl->monitorPassword); + sformat(connstr, sizeof(connstr), + "host=%s port=5432 dbname=pg_auto_failover " + "user=autoctl_node password=%s connect_timeout=5", + svc, cl->monitorPassword); } else { - snprintf(connstr, sizeof(connstr), - "host=%s port=5432 dbname=pg_auto_failover " - "user=autoctl_node connect_timeout=5", - svc); + sformat(connstr, sizeof(connstr), + "host=%s port=5432 dbname=pg_auto_failover " + "user=autoctl_node connect_timeout=5", + svc); } } else @@ -765,19 +770,19 @@ runner_notify_connect(TestRunner *r) } if (cl->monitorPassword[0]) { - snprintf(connstr, sizeof(connstr), - "host=localhost port=%d dbname=pg_auto_failover " - "user=autoctl_node password=%s " - "connect_timeout=5", - port, cl->monitorPassword); + sformat(connstr, sizeof(connstr), + "host=localhost port=%d dbname=pg_auto_failover " + "user=autoctl_node password=%s " + "connect_timeout=5", + port, cl->monitorPassword); } else { - snprintf(connstr, sizeof(connstr), - "host=localhost port=%d dbname=pg_auto_failover " - "user=autoctl_node " - "connect_timeout=2", - port); + sformat(connstr, sizeof(connstr), + "host=localhost port=%d dbname=pg_auto_failover " + "user=autoctl_node " + "connect_timeout=2", + port); } } @@ -1207,7 +1212,7 @@ wait_for_states(TestRunner *r, TestCmd *cmd) for (int i = 0; i < cmd->waitGroupCount; i++) { char g[16]; - snprintf(g, sizeof(g), "%s%d", i > 0 ? "," : "", cmd->waitGroups[i]); + sformat(g, sizeof(g), "%s%d", i > 0 ? "," : "", cmd->waitGroups[i]); strlcat(label, g, sizeof(label)); } } @@ -1806,10 +1811,10 @@ runner_check_stays_notify(TestRunner *r, strcmp(ns_goal, expectedState) != 0) { PQfreemem(notify); - snprintf(errBuf, errLen, - "stays-while: %s was assigned state %s " - "(expected to stay %s)", - nodeName, ns_goal, expectedState); + sformat(errBuf, errLen, + "stays-while: %s was assigned state %s " + "(expected to stay %s)", + nodeName, ns_goal, expectedState); return false; } } @@ -1998,8 +2003,8 @@ runner_expand_macros(TestRunner *r, const char *args, char *dst, int dstlen, r->composeBase); if (rc != 0 || cidr[0] == '\0') { - snprintf(errBuf, errLen, - "%%CIDR%%: could not get Docker network CIDR from monitor"); + sformat(errBuf, errLen, + "%%CIDR%%: could not get Docker network CIDR from monitor"); return false; } @@ -2065,9 +2070,9 @@ runner_exec_cmd(TestRunner *r, TestCmd *cmd, char *errBuf, int errLen) { log_output(" ", out); } - snprintf(errBuf, errLen, - "exec %s %s failed (exit %d)", - cmd->service, expandedArgs, rc); + sformat(errBuf, errLen, + "exec %s %s failed (exit %d)", + cmd->service, expandedArgs, rc); return false; } @@ -2102,10 +2107,10 @@ runner_exec_cmd(TestRunner *r, TestCmd *cmd, char *errBuf, int errLen) if (!monitor_wait_formation_states(r, fsStates, 2, NULL, 0, 120)) { - snprintf(errBuf, errLen, - "exec %s %s: timed out waiting for " - "primary+secondary", - cmd->service, expandedArgs); + sformat(errBuf, errLen, + "exec %s %s: timed out waiting for " + "primary+secondary", + cmd->service, expandedArgs); return false; } } @@ -2125,9 +2130,9 @@ runner_exec_cmd(TestRunner *r, TestCmd *cmd, char *errBuf, int errLen) } if (!wait_for_state(r, cmd->service, "maintenance", 60, false)) { - snprintf(errBuf, errLen, - "exec %s %s: timed out waiting for maintenance", - cmd->service, expandedArgs); + sformat(errBuf, errLen, + "exec %s %s: timed out waiting for maintenance", + cmd->service, expandedArgs); return false; } } @@ -2147,10 +2152,10 @@ runner_exec_cmd(TestRunner *r, TestCmd *cmd, char *errBuf, int errLen) { log_output(" ", out); } - snprintf(errBuf, errLen, - "exec-fails %s %s: command succeeded (exit 0) " - "but expected failure", - cmd->service, cmd->args); + sformat(errBuf, errLen, + "exec-fails %s %s: command succeeded (exit 0) " + "but expected failure", + cmd->service, cmd->args); return false; } log_debug("exec-fails %s %s: exited with %d (expected)", @@ -2175,9 +2180,9 @@ runner_exec_cmd(TestRunner *r, TestCmd *cmd, char *errBuf, int errLen) seenThrough, cmd->passThroughCount, cmd->timeoutSeconds)) { - snprintf(errBuf, errLen, - "timeout: %s assigned-state never reached %s", - cmd->service, cmd->state); + sformat(errBuf, errLen, + "timeout: %s assigned-state never reached %s", + cmd->service, cmd->state); return false; } @@ -2185,12 +2190,12 @@ runner_exec_cmd(TestRunner *r, TestCmd *cmd, char *errBuf, int errLen) { if (!seenThrough[i]) { - snprintf(errBuf, errLen, - "%s did not pass through assigned-state %s " - "on way to %s", - cmd->service, - cmd->passThroughStates[i], - cmd->state); + sformat(errBuf, errLen, + "%s did not pass through assigned-state %s " + "on way to %s", + cmd->service, + cmd->passThroughStates[i], + cmd->state); return false; } } @@ -2211,8 +2216,8 @@ runner_exec_cmd(TestRunner *r, TestCmd *cmd, char *errBuf, int errLen) } strlcat(label, cmd->waitStates[i], sizeof(label)); } - snprintf(errBuf, errLen, - "timeout: formation never reached states {%s}", label); + sformat(errBuf, errLen, + "timeout: formation never reached states {%s}", label); return false; } return true; @@ -2224,9 +2229,9 @@ runner_exec_cmd(TestRunner *r, TestCmd *cmd, char *errBuf, int errLen) { if (!runner_promote_one(r, cmd->promoteNodes[i])) { - snprintf(errBuf, errLen, - "promote: could not make %s primary", - cmd->promoteNodes[i]); + sformat(errBuf, errLen, + "promote: could not make %s primary", + cmd->promoteNodes[i]); return false; } } @@ -2243,12 +2248,12 @@ runner_exec_cmd(TestRunner *r, TestCmd *cmd, char *errBuf, int errLen) cmd->timeoutSeconds, cmd->kind == CMD_ASSERT_ASSIGNED)) { - snprintf(errBuf, errLen, - "timeout: %s %s never reached %s", - cmd->service, - cmd->kind == CMD_ASSERT_ASSIGNED - ? "assigned-state" : "state", - cmd->state); + sformat(errBuf, errLen, + "timeout: %s %s never reached %s", + cmd->service, + cmd->kind == CMD_ASSERT_ASSIGNED + ? "assigned-state" : "state", + cmd->state); return false; } return true; @@ -2259,21 +2264,21 @@ runner_exec_cmd(TestRunner *r, TestCmd *cmd, char *errBuf, int errLen) reported, sizeof(reported), assigned, sizeof(assigned))) { - snprintf(errBuf, errLen, - "cannot reach monitor to assert %s state", - cmd->service); + sformat(errBuf, errLen, + "cannot reach monitor to assert %s state", + cmd->service); return false; } const char *actual = (cmd->kind == CMD_ASSERT_ASSIGNED) ? assigned : reported; if (strcmp(actual, cmd->state) != 0) { - snprintf(errBuf, errLen, - "%s %s is \"%s\", expected \"%s\"", - cmd->service, - (cmd->kind == CMD_ASSERT_ASSIGNED) - ? "assigned-state" : "state", - actual, cmd->state); + sformat(errBuf, errLen, + "%s %s is \"%s\", expected \"%s\"", + cmd->service, + (cmd->kind == CMD_ASSERT_ASSIGNED) + ? "assigned-state" : "state", + actual, cmd->state); return false; } return true; @@ -2306,8 +2311,8 @@ runner_exec_cmd(TestRunner *r, TestCmd *cmd, char *errBuf, int errLen) r->lastSqlOutput[0] = '\0'; return true; } - snprintf(errBuf, errLen, - "sql on %s failed:\n%s", cmd->service, r->lastSqlOutput); + sformat(errBuf, errLen, + "sql on %s failed:\n%s", cmd->service, r->lastSqlOutput); return false; } log_debug("sql output: %s", r->lastSqlOutput); @@ -2324,9 +2329,9 @@ runner_exec_cmd(TestRunner *r, TestCmd *cmd, char *errBuf, int errLen) */ if (strstr(r->lastSqlOutput, cmd->expected) == NULL) { - snprintf(errBuf, errLen, - "expected \"%s\", got \"%s\"", - cmd->expected, r->lastSqlOutput); + sformat(errBuf, errLen, + "expected \"%s\", got \"%s\"", + cmd->expected, r->lastSqlOutput); return false; } return true; @@ -2336,18 +2341,18 @@ runner_exec_cmd(TestRunner *r, TestCmd *cmd, char *errBuf, int errLen) { if (!r->lastSqlFailed) { - snprintf(errBuf, errLen, - "expected SQL error on %s, but the query succeeded", - r->lastSqlService); + sformat(errBuf, errLen, + "expected SQL error on %s, but the query succeeded", + r->lastSqlService); return false; } if (cmd->state[0] && strcmp(r->lastSqlState, cmd->state) != 0) { - snprintf(errBuf, errLen, - "expected SQLSTATE %s on %s, got %s", - cmd->state, r->lastSqlService, - r->lastSqlState[0] ? r->lastSqlState : "(unknown)"); + sformat(errBuf, errLen, + "expected SQLSTATE %s on %s, got %s", + cmd->state, r->lastSqlService, + r->lastSqlState[0] ? r->lastSqlState : "(unknown)"); return false; } r->lastSqlFailed = false; /* consumed */ @@ -2358,8 +2363,8 @@ runner_exec_cmd(TestRunner *r, TestCmd *cmd, char *errBuf, int errLen) { if (!runner_network_off(r, cmd->service)) { - snprintf(errBuf, errLen, - "network disconnect %s failed", cmd->service); + sformat(errBuf, errLen, + "network disconnect %s failed", cmd->service); return false; } return true; @@ -2369,8 +2374,8 @@ runner_exec_cmd(TestRunner *r, TestCmd *cmd, char *errBuf, int errLen) { if (!runner_network_on(r, cmd->service)) { - snprintf(errBuf, errLen, - "network connect %s failed", cmd->service); + sformat(errBuf, errLen, + "network connect %s failed", cmd->service); return false; } return true; @@ -2411,9 +2416,9 @@ runner_exec_cmd(TestRunner *r, TestCmd *cmd, char *errBuf, int errLen) } if (rc != 0) { - snprintf(errBuf, errLen, - "docker compose start %s failed (exit %d)", - cmd->service, rc); + sformat(errBuf, errLen, + "docker compose start %s failed (exit %d)", + cmd->service, rc); return false; } return true; @@ -2431,9 +2436,9 @@ runner_exec_cmd(TestRunner *r, TestCmd *cmd, char *errBuf, int errLen) } if (rc != 0) { - snprintf(errBuf, errLen, - "docker compose stop %s failed (exit %d)", - cmd->service, rc); + sformat(errBuf, errLen, + "docker compose stop %s failed (exit %d)", + cmd->service, rc); return false; } return true; @@ -2451,9 +2456,9 @@ runner_exec_cmd(TestRunner *r, TestCmd *cmd, char *errBuf, int errLen) } if (rc != 0) { - snprintf(errBuf, errLen, - "docker compose kill %s failed (exit %d)", - cmd->service, rc); + sformat(errBuf, errLen, + "docker compose kill %s failed (exit %d)", + cmd->service, rc); return false; } return true; @@ -2490,9 +2495,9 @@ runner_exec_cmd(TestRunner *r, TestCmd *cmd, char *errBuf, int errLen) } if (rc != 0) { - snprintf(errBuf, errLen, - "compose inject: docker create %s failed (exit %d)", - cmd->expected, rc); + sformat(errBuf, errLen, + "compose inject: docker create %s failed (exit %d)", + cmd->expected, rc); return false; } @@ -2510,8 +2515,8 @@ runner_exec_cmd(TestRunner *r, TestCmd *cmd, char *errBuf, int errLen) "docker rm _pgaf_inject_tmp 2>&1"); if (rc != 0) { - snprintf(errBuf, errLen, - "compose inject: docker cp from image failed (exit %d)", rc); + sformat(errBuf, errLen, + "compose inject: docker cp from image failed (exit %d)", rc); return false; } @@ -2529,9 +2534,9 @@ runner_exec_cmd(TestRunner *r, TestCmd *cmd, char *errBuf, int errLen) } if (rc != 0) { - snprintf(errBuf, errLen, - "compose inject: docker cp to %s:%s failed (exit %d)", - cmd->service, cmd->state, rc); + sformat(errBuf, errLen, + "compose inject: docker cp to %s:%s failed (exit %d)", + cmd->service, cmd->state, rc); return false; } @@ -2647,12 +2652,12 @@ runner_exec_cmd(TestRunner *r, TestCmd *cmd, char *errBuf, int errLen) strlcat(label, " and ", sizeof(label)); } char part[128]; - snprintf(part, sizeof(part), "%s=%s", - cmd->waitNodes[i], cmd->waitStates[i]); + sformat(part, sizeof(part), "%s=%s", + cmd->waitNodes[i], cmd->waitStates[i]); strlcat(label, part, sizeof(label)); } - snprintf(errBuf, errLen, - "timeout: conditions not met: %s", label); + sformat(errBuf, errLen, + "timeout: conditions not met: %s", label); return false; } @@ -2677,8 +2682,8 @@ runner_exec_cmd(TestRunner *r, TestCmd *cmd, char *errBuf, int errLen) { log_output(" ", out); } - snprintf(errBuf, errLen, - "pg_autoctl %s failed (exit %d)", cmd->args, rc); + sformat(errBuf, errLen, + "pg_autoctl %s failed (exit %d)", cmd->args, rc); return false; } return true; @@ -2724,9 +2729,9 @@ runner_exec_cmd(TestRunner *r, TestCmd *cmd, char *errBuf, int errLen) usleep(500000); /* 500ms */ } - snprintf(errBuf, errLen, - "timeout: service %s did not stop within %ds", - svc, timeoutSecs); + sformat(errBuf, errLen, + "timeout: service %s did not stop within %ds", + svc, timeoutSecs); return false; } @@ -2743,9 +2748,9 @@ runner_exec_cmd(TestRunner *r, TestCmd *cmd, char *errBuf, int errLen) r->composeBase, cmd->service); if (rc != 0) { - snprintf(errBuf, errLen, - "stop postgres %s failed (exit %d)", - cmd->service, rc); + sformat(errBuf, errLen, + "stop postgres %s failed (exit %d)", + cmd->service, rc); return false; } return true; @@ -2760,9 +2765,9 @@ runner_exec_cmd(TestRunner *r, TestCmd *cmd, char *errBuf, int errLen) r->composeBase, cmd->service); if (rc != 0) { - snprintf(errBuf, errLen, - "start postgres %s failed (exit %d)", - cmd->service, rc); + sformat(errBuf, errLen, + "start postgres %s failed (exit %d)", + cmd->service, rc); return false; } return true; @@ -2797,16 +2802,16 @@ runner_exec_cmd(TestRunner *r, TestCmd *cmd, char *errBuf, int errLen) curReported, sizeof(curReported), curAssigned, sizeof(curAssigned))) { - snprintf(errBuf, errLen, - "stays-while: could not get state of %s", cmd->service); + sformat(errBuf, errLen, + "stays-while: could not get state of %s", cmd->service); return false; } if (strcmp(curAssigned, cmd->state) != 0) { - snprintf(errBuf, errLen, - "stays-while: %s assigned-state is %s, " - "expected %s (before body)", - cmd->service, curAssigned, cmd->state); + sformat(errBuf, errLen, + "stays-while: %s assigned-state is %s, " + "expected %s (before body)", + cmd->service, curAssigned, cmd->state); return false; } @@ -2838,17 +2843,17 @@ runner_exec_cmd(TestRunner *r, TestCmd *cmd, char *errBuf, int errLen) curReported, sizeof(curReported), curAssigned, sizeof(curAssigned))) { - snprintf(errBuf, errLen, - "stays-while: could not get state of %s after command", - cmd->service); + sformat(errBuf, errLen, + "stays-while: could not get state of %s after command", + cmd->service); return false; } if (strcmp(curAssigned, cmd->state) != 0) { - snprintf(errBuf, errLen, - "stays-while: %s assigned-state changed to %s, " - "expected %s during body", - cmd->service, curAssigned, cmd->state); + sformat(errBuf, errLen, + "stays-while: %s assigned-state changed to %s, " + "expected %s during body", + cmd->service, curAssigned, cmd->state); return false; } } @@ -2867,10 +2872,10 @@ runner_exec_cmd(TestRunner *r, TestCmd *cmd, char *errBuf, int errLen) curAssigned, sizeof(curAssigned)) || strcmp(curAssigned, cmd->state) != 0) { - snprintf(errBuf, errLen, - "stays-while: %s assigned-state is %s " - "after body, expected %s", - cmd->service, curAssigned, cmd->state); + sformat(errBuf, errLen, + "stays-while: %s assigned-state is %s " + "after body, expected %s", + cmd->service, curAssigned, cmd->state); return false; } } @@ -2941,12 +2946,12 @@ runner_exec_cmd(TestRunner *r, TestCmd *cmd, char *errBuf, int errLen) } char grepCmd[8192]; - snprintf(grepCmd, sizeof(grepCmd), - "docker compose --project-directory %s logs --no-color %s 2>&1 | grep %s %s", - r->workDir, - cmd->service, - flag, - quotedPat); + sformat(grepCmd, sizeof(grepCmd), + "docker compose --project-directory %s logs --no-color %s 2>&1 | grep %s %s", + r->workDir, + cmd->service, + flag, + quotedPat); log_info(" logs %s: %s%s \"%s\"", cmd->service, @@ -3061,20 +3066,20 @@ cmd_label(const TestCmd *cmd, char *buf, int len) { case CMD_EXEC: { - snprintf(buf, len, "exec %s %s", cmd->service, cmd->args); + sformat(buf, len, "exec %s %s", cmd->service, cmd->args); break; } case CMD_EXEC_FAILS: { - snprintf(buf, len, "exec-fails %s %s", cmd->service, cmd->args); + sformat(buf, len, "exec-fails %s %s", cmd->service, cmd->args); break; } case CMD_WAIT_STATE: { - snprintf(buf, len, "wait until %s state = %s timeout %ds", - cmd->service, cmd->state, cmd->timeoutSeconds); + sformat(buf, len, "wait until %s state = %s timeout %ds", + cmd->service, cmd->state, cmd->timeoutSeconds); break; } @@ -3089,21 +3094,21 @@ cmd_label(const TestCmd *cmd, char *buf, int len) } strlcat(states, cmd->waitStates[i], sizeof(states)); } - snprintf(buf, len, "wait until %s timeout %ds", states, - cmd->timeoutSeconds); + sformat(buf, len, "wait until %s timeout %ds", states, + cmd->timeoutSeconds); break; } case CMD_ASSERT_STATE: { - snprintf(buf, len, "assert %s state = %s", cmd->service, cmd->state); + sformat(buf, len, "assert %s state = %s", cmd->service, cmd->state); break; } case CMD_ASSERT_ASSIGNED: { - snprintf(buf, len, "assert %s assigned-state = %s", - cmd->service, cmd->state); + sformat(buf, len, "assert %s assigned-state = %s", + cmd->service, cmd->state); break; } @@ -3118,14 +3123,14 @@ cmd_label(const TestCmd *cmd, char *buf, int len) } strlcat(nodes, cmd->promoteNodes[i], sizeof(nodes)); } - snprintf(buf, len, "promote %s", nodes[0] ? nodes : cmd->service); + sformat(buf, len, "promote %s", nodes[0] ? nodes : cmd->service); break; } case CMD_SQL: { inline_text(cmd->args, tmp, sizeof(tmp)); - snprintf(buf, len, "sql %s { %s }", cmd->service, tmp); + sformat(buf, len, "sql %s { %s }", cmd->service, tmp); break; } @@ -3141,84 +3146,84 @@ cmd_label(const TestCmd *cmd, char *buf, int len) { const char *nl = strchr(p, '\n'); int rowlen = nl ? (int) (nl - p) : (int) strlen(p); - int n = snprintf(rows + rpos, sizeof(rows) - rpos, - "%s{ %.*s }", - rpos ? " " : "", rowlen, p); + int n = sformat(rows + rpos, sizeof(rows) - rpos, + "%s{ %.*s }", + rpos ? " " : "", rowlen, p); if (n > 0) { rpos += n; } p = nl ? nl + 1 : p + rowlen; } - snprintf(buf, len, "expect { %s }", rows); + sformat(buf, len, "expect { %s }", rows); } else { inline_text(cmd->expected, tmp, sizeof(tmp)); - snprintf(buf, len, "expect { %s }", tmp); + sformat(buf, len, "expect { %s }", tmp); } break; } case CMD_EXPECT_ERROR: { - snprintf(buf, len, "expect error %s", cmd->state); + sformat(buf, len, "expect error %s", cmd->state); break; } case CMD_NETWORK_OFF: { - snprintf(buf, len, "network disconnect %s", cmd->service); + sformat(buf, len, "network disconnect %s", cmd->service); break; } case CMD_NETWORK_ON: { - snprintf(buf, len, "network connect %s", cmd->service); + sformat(buf, len, "network connect %s", cmd->service); break; } case CMD_SLEEP: { - snprintf(buf, len, "sleep %ds", cmd->timeoutSeconds); + sformat(buf, len, "sleep %ds", cmd->timeoutSeconds); break; } case CMD_COMPOSE_DOWN: { - snprintf(buf, len, "compose down"); + sformat(buf, len, "compose down"); break; } case CMD_COMPOSE_START: { - snprintf(buf, len, "compose start %s", cmd->service); + sformat(buf, len, "compose start %s", cmd->service); break; } case CMD_COMPOSE_STOP: { - snprintf(buf, len, "compose stop %s", cmd->service); + sformat(buf, len, "compose stop %s", cmd->service); break; } case CMD_COMPOSE_KILL: { - snprintf(buf, len, "compose kill %s", cmd->service); + sformat(buf, len, "compose kill %s", cmd->service); break; } case CMD_COMPOSE_INJECT: { - snprintf(buf, len, "compose inject %s %s %s:%s", - cmd->expected, cmd->args, cmd->service, cmd->state); + sformat(buf, len, "compose inject %s %s %s:%s", + cmd->expected, cmd->args, cmd->service, cmd->state); break; } case CMD_WAIT_STOPPED: { - snprintf(buf, len, "wait until %s stopped timeout %ds", - cmd->service, cmd->timeoutSeconds); + sformat(buf, len, "wait until %s stopped timeout %ds", + cmd->service, cmd->timeoutSeconds); break; } @@ -3232,59 +3237,59 @@ cmd_label(const TestCmd *cmd, char *buf, int len) strlcat(parts, " and ", sizeof(parts)); } char piece[128]; - snprintf(piece, sizeof(piece), "%s state = %s", - cmd->waitNodes[i], cmd->waitStates[i]); + sformat(piece, sizeof(piece), "%s state = %s", + cmd->waitNodes[i], cmd->waitStates[i]); strlcat(parts, piece, sizeof(parts)); } - snprintf(buf, len, "wait until %s timeout %ds", parts, - cmd->timeoutSeconds); + sformat(buf, len, "wait until %s timeout %ds", parts, + cmd->timeoutSeconds); break; } case CMD_PG_AUTOCTL: { - snprintf(buf, len, "pg_autoctl %s", cmd->args); + sformat(buf, len, "pg_autoctl %s", cmd->args); break; } case CMD_STOP_POSTGRES: { - snprintf(buf, len, "stop postgres %s", cmd->service); + sformat(buf, len, "stop postgres %s", cmd->service); break; } case CMD_START_POSTGRES: { - snprintf(buf, len, "start postgres %s", cmd->service); + sformat(buf, len, "start postgres %s", cmd->service); break; } case CMD_STAYS_WHILE: { - snprintf(buf, len, "assert %s stays %s while { ... }", - cmd->service, cmd->state); + sformat(buf, len, "assert %s stays %s while { ... }", + cmd->service, cmd->state); break; } case CMD_SET_MONITOR: { - snprintf(buf, len, "set monitor %s", cmd->service); + sformat(buf, len, "set monitor %s", cmd->service); break; } case CMD_LOGS_CHECK: { - snprintf(buf, len, "logs %s %s%s \"%s\"", - cmd->service, - cmd->logsNegate ? "not " : "", - cmd->allowError ? "matches" : "contains", - cmd->args); + sformat(buf, len, "logs %s %s%s \"%s\"", + cmd->service, + cmd->logsNegate ? "not " : "", + cmd->allowError ? "matches" : "contains", + cmd->args); break; } default: { - snprintf(buf, len, "(unknown command %d)", cmd->kind); + sformat(buf, len, "(unknown command %d)", cmd->kind); break; } } @@ -3375,10 +3380,10 @@ runner_wait_for_monitor(TestRunner *r) time_t deadline = time(NULL) + MONITOR_WAIT_TIMEOUT_SECS; char monitorReadyCmd[sizeof(r->composeBase) + 128]; - snprintf(monitorReadyCmd, sizeof(monitorReadyCmd), - "%s exec -T monitor psql -U autoctl_node -d pg_auto_failover " - "-c 'SELECT 1' >/dev/null 2>&1", - r->composeBase); + sformat(monitorReadyCmd, sizeof(monitorReadyCmd), + "%s exec -T monitor psql -U autoctl_node -d pg_auto_failover " + "-c 'SELECT 1' >/dev/null 2>&1", + r->composeBase); while (time(NULL) < deadline) { @@ -3437,7 +3442,7 @@ runner_run(TestSpec *spec, const char *workDir, bool noCleanup) * include a pgaftest service and we're running inside that container; skip * compose up since the stack is already running. */ - if (!getenv("PGAFTEST_COMPOSE_SERVICE")) + if (!getenv("PGAFTEST_COMPOSE_SERVICE")) /* IGNORE-BANNED */ { log_info("Starting compose stack (project: %s)", r.projectName); @@ -3610,17 +3615,17 @@ runner_setup(TestSpec *spec, const char *workDir, bool withTmux) if (spec->setup) { - snprintf(bottomCmd, sizeof(bottomCmd), - "%s _setup_ %s --work-dir %s && " - "%s exec -it %s bash", - pg_autoctl_program, spec->filename, workDir, - r.composeBase, shellNode); + sformat(bottomCmd, sizeof(bottomCmd), + "%s _setup_ %s --work-dir %s && " + "%s exec -it %s bash", + pg_autoctl_program, spec->filename, workDir, + r.composeBase, shellNode); } else { - snprintf(bottomCmd, sizeof(bottomCmd), - "%s exec -it %s bash", - r.composeBase, shellNode); + sformat(bottomCmd, sizeof(bottomCmd), + "%s exec -it %s bash", + r.composeBase, shellNode); } log_info("Starting tmux session \"%s\" (shell target: %s)", @@ -3657,15 +3662,15 @@ runner_setup(TestSpec *spec, const char *workDir, bool withTmux) } } - printf("\nCluster ready — compose project: %s\n", r.projectName); - printf("Work dir: %s\n", workDir); - printf("\nAvailable steps:"); + fformat(stdout, "\nCluster ready — compose project: %s\n", r.projectName); + fformat(stdout, "Work dir: %s\n", workDir); + fformat(stdout, "\nAvailable steps:"); for (TestStep *s = spec->steps; s; s = s->next) { - printf(" %s", s->name); + fformat(stdout, " %s", s->name); } - printf("\n\nRun a step: pgaftest step --work-dir %s\n", workDir); - printf("Tear down: pgaftest down --work-dir %s\n\n", workDir); + fformat(stdout, "\n\nRun a step: pgaftest step --work-dir %s\n", workDir); + fformat(stdout, "Tear down: pgaftest down --work-dir %s\n\n", workDir); } return true; @@ -3795,12 +3800,12 @@ runner_prepare(TestSpec *spec, const char *outDir) const char *slash = strrchr(spec->filename, '/'); if (slash) { - snprintf(derivedDir, sizeof(derivedDir), "%.*s/%s-compose", - (int) (slash - spec->filename), spec->filename, stem); + sformat(derivedDir, sizeof(derivedDir), "%.*s/%s-compose", + (int) (slash - spec->filename), spec->filename, stem); } else { - snprintf(derivedDir, sizeof(derivedDir), "%s-compose", stem); + sformat(derivedDir, sizeof(derivedDir), "%s-compose", stem); } outDir = derivedDir; } @@ -3859,14 +3864,14 @@ runner_prepare(TestSpec *spec, const char *outDir) /* Write Makefile */ char makefilePath[1280]; - snprintf(makefilePath, sizeof(makefilePath), "%s/Makefile", outDir); - FILE *mf = fopen(makefilePath, "w"); + sformat(makefilePath, sizeof(makefilePath), "%s/Makefile", outDir); + FILE *mf = fopen(makefilePath, "w"); /* IGNORE-BANNED */ if (!mf) { log_error("Failed to open \"%s\": %m", makefilePath); return false; } - fprintf(mf, + fformat(mf, ".PHONY: test down\n" "\n" "test:\n" @@ -3881,10 +3886,10 @@ runner_prepare(TestSpec *spec, const char *outDir) log_info("Wrote Makefile to \"%s\"", makefilePath); /* Print the docker compose command to stdout */ - printf("cd %s && \\\n", outDir); - printf(" docker compose -p %s -f docker-compose.yml" - " up --build --exit-code-from pgaftest --attach pgaftest\n", - r.projectName); + fformat(stdout, "cd %s && \\\n", outDir); + fformat(stdout, " docker compose -p %s -f docker-compose.yml" + " up --build --exit-code-from pgaftest --attach pgaftest\n", + r.projectName); return true; } diff --git a/src/bin/pgaftest/test_spec_parse.c b/src/bin/pgaftest/test_spec_parse.c index b7f3b6e33..2fa5e85fa 100644 --- a/src/bin/pgaftest/test_spec_parse.c +++ b/src/bin/pgaftest/test_spec_parse.c @@ -325,8 +325,8 @@ static TestSpec *current_spec = NULL; static void yyerror(const char *msg) { - fprintf(stderr, "pgaftest: parse error at line %d: %s\n", - pgaf_line_number, msg); + fprintf(/* IGNORE-BANNED */ stderr, "pgaftest: parse error at line %d: %s\n", + pgaf_line_number, msg); exit(1); } @@ -1376,9 +1376,9 @@ static const yytype_uint8 yystos[] = #ifndef YY_LOCATION_PRINT # if defined YYLTYPE_IS_TRIVIAL && YYLTYPE_IS_TRIVIAL # define YY_LOCATION_PRINT(File, Loc) \ - fprintf(File, "%d.%d-%d.%d", \ - (Loc).first_line, (Loc).first_column, \ - (Loc).last_line, (Loc).last_column) + fprintf(/* IGNORE-BANNED */ File, "%d.%d-%d.%d", \ + (Loc).first_line, (Loc).first_column, \ + (Loc).last_line, (Loc).last_column) # else # define YY_LOCATION_PRINT(File, Loc) ((void) 0) # endif @@ -1535,11 +1535,11 @@ int yyrule; /* The symbols being reduced. */ for (yyi = 0; yyi < yynrhs; yyi++) { - fprintf(stderr, " $%d = ", yyi + 1); + fprintf(stderr, " $%d = ", yyi + 1); /* IGNORE-BANNED */ yy_symbol_print(stderr, yyrhs[yyprhs[yyrule] + yyi], &(yyvsp[(yyi + 1) - (yynrhs)]) ); - fprintf(stderr, "\n"); + fprintf(stderr, "\n"); /* IGNORE-BANNED */ } } @@ -2346,8 +2346,9 @@ yyparse() TestCluster *cl = ¤t_spec->cluster; if (cl->formationCount >= PGAF_MAX_FORMATIONS) { - fprintf(stderr, "pgaftest: too many formations (max %d)\n", - PGAF_MAX_FORMATIONS); + fprintf(/* IGNORE-BANNED */ stderr, + "pgaftest: too many formations (max %d)\n", + PGAF_MAX_FORMATIONS); exit(1); } current_formation = &cl->formations[cl->formationCount++]; @@ -2427,8 +2428,9 @@ yyparse() { if (current_formation->nodeCount >= PGAF_MAX_NODES) { - fprintf(stderr, "pgaftest: too many nodes in formation (max %d)\n", - PGAF_MAX_NODES); + fprintf(/* IGNORE-BANNED */ stderr, + "pgaftest: too many nodes in formation (max %d)\n", + PGAF_MAX_NODES); exit(1); } current_node = ¤t_formation->nodes[current_formation->nodeCount++]; @@ -3286,8 +3288,9 @@ yyparse() { /* SQLSTATE codes like 25006 are all digits, lexed as T_INTEGER */ (yyval.cmd) = make_cmd(CMD_EXPECT_ERROR); - snprintf((yyval.cmd)->state, sizeof((yyval.cmd)->state), "%d", - (yyvsp[(3) - (3)].ival)); + snprintf(/* IGNORE-BANNED */ (yyval.cmd)->state, + sizeof((yyval.cmd)->state), "%d", + (yyvsp[(3) - (3)].ival)); } break; @@ -3485,11 +3488,11 @@ yyparse() /* only "set monitor " is supported; $2 must be "monitor" */ if (strcmp((yyvsp[(2) - (3)].str), "monitor") != 0) { - fprintf(stderr, - "pgaftest: unknown 'set' target '%s' (expected 'monitor')\n", - (yyvsp[(2) - - ( - 3)].str)); + fprintf(/* IGNORE-BANNED */ stderr, + "pgaftest: unknown 'set' target '%s' (expected 'monitor')\n", + (yyvsp[(2) - + ( + 3)].str)); free((yyvsp[(2) - (3)].str)); free((yyvsp[(3) - (3)].str)); YYERROR; @@ -3574,8 +3577,9 @@ yyparse() } else { - fprintf(stderr, "pgaftest: too many steps in sequence (max %d)\n", - PGAF_MAX_SEQ); + fprintf(/* IGNORE-BANNED */ stderr, + "pgaftest: too many steps in sequence (max %d)\n", + PGAF_MAX_SEQ); exit(1); } } @@ -4001,18 +4005,19 @@ yyparse() TestSpec * parse_test_spec(const char *filename) { - FILE *f = fopen(filename, "r"); + FILE *f = fopen(filename, "r"); /* IGNORE-BANNED */ if (!f) { - fprintf(stderr, "pgaftest: cannot open spec file \"%s\": %s\n", - filename, strerror(errno)); + fprintf(/* IGNORE-BANNED */ stderr, + "pgaftest: cannot open spec file \"%s\": %s\n", + filename, strerror(errno) /* IGNORE-BANNED */); return NULL; } TestSpec *spec = (TestSpec *) calloc(1, sizeof(TestSpec)); if (!spec) { - fprintf(stderr, "out of memory\n"); + fprintf(stderr, "out of memory\n"); /* IGNORE-BANNED */ exit(1); } @@ -4034,7 +4039,7 @@ make_cmd(TestCmdKind kind) TestCmd *c = (TestCmd *) calloc(1, sizeof(TestCmd)); if (!c) { - fprintf(stderr, "out of memory\n"); + fprintf(stderr, "out of memory\n"); /* IGNORE-BANNED */ exit(1); } c->kind = kind; @@ -4049,7 +4054,7 @@ make_step(const char *name) TestStep *s = (TestStep *) calloc(1, sizeof(TestStep)); if (!s) { - fprintf(stderr, "out of memory\n"); + fprintf(stderr, "out of memory\n"); /* IGNORE-BANNED */ exit(1); } if (name) diff --git a/src/bin/pgaftest/test_spec_scan.c b/src/bin/pgaftest/test_spec_scan.c index e93ed2785..ecadc3603 100644 --- a/src/bin/pgaftest/test_spec_scan.c +++ b/src/bin/pgaftest/test_spec_scan.c @@ -1190,7 +1190,7 @@ pgaf_strdup(const char *s) char *r = strdup(s); if (!r) { - fprintf(stderr, "out of memory\n"); + fprintf(stderr, "out of memory\n"); /* IGNORE-BANNED */ exit(1); } return r; @@ -1590,7 +1590,7 @@ YY_DECL YY_RULE_SETUP #line 106 "test_spec_scan.l" { - yylval.ival = atoi(yytext); + yylval.ival = atoi(yytext); /* IGNORE-BANNED */ return T_INTEGER; } @@ -1635,8 +1635,9 @@ YY_DECL } /* fallback: should not happen in a well-formed file */ - fprintf(stderr, "pgaftest: unexpected '{' at line %d\n", - pgaf_line_number); + fprintf(/* IGNORE-BANNED */ stderr, + "pgaftest: unexpected '{' at line %d\n", + pgaf_line_number); } YY_BREAK @@ -1661,9 +1662,9 @@ YY_DECL YY_RULE_SETUP #line 143 "test_spec_scan.l" { - fprintf(stderr, - "pgaftest: unexpected character '%c' at line %d\n", - yytext[0], pgaf_line_number); + fprintf(/* IGNORE-BANNED */ stderr, + "pgaftest: unexpected character '%c' at line %d\n", + yytext[0], pgaf_line_number); } YY_BREAK @@ -1959,7 +1960,7 @@ YY_DECL YY_RULE_SETUP #line 188 "test_spec_scan.l" { - yylval.ival = atoi(yytext); + yylval.ival = atoi(yytext); /* IGNORE-BANNED */ return T_INTEGER; } @@ -2631,7 +2632,7 @@ YY_DECL YY_RULE_SETUP #line 297 "test_spec_scan.l" { - yylval.ival = atoi(yytext); + yylval.ival = atoi(yytext); /* IGNORE-BANNED */ return T_INTEGER; } @@ -3623,7 +3624,7 @@ yy_scan_bytes(const char *yybytes, yy_size_t _yybytes_len) static void yynoreturn yy_fatal_error(const char *msg) { - fprintf(stderr, "%s\n", msg); + fprintf(stderr, "%s\n", msg); /* IGNORE-BANNED */ exit(YY_EXIT_FAILURE); } From 98e746e769834658f9da3d5c2a990a6812fe2df1 Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Tue, 7 Jul 2026 14:07:04 +0200 Subject: [PATCH 26/68] ci: fix per-version CITUSTAG in run-pgaftest.yml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Citus v12.x supports PG14–15; v13.x supports PG15–17. No single tag covers the full matrix. The top-level Makefile already handles this: BUILD_ARGS_pg14 = --build-arg CITUSTAG=v12.1.5 BUILD_ARGS_pg15 = --build-arg CITUSTAG=v12.1.5 BUILD_ARGS_pg16+ = --build-arg CITUSTAG=$(CITUSTAG) # v13.2.0 run-pgaftest.yml was calling docker build with no CITUSTAG override, so it always used the Dockerfile ARG default and broke on either end: - v13.0.1 → configure: Citus not compatible with PG14 - v12.1.5 → configure: Citus not compatible with PG17 Fix: inline the same version map in the workflow using a shell case statement. Also bump the Dockerfile ARG default to v13.2.0 to match the Makefile. --- .github/workflows/run-pgaftest.yml | 9 +++++++++ Dockerfile | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/run-pgaftest.yml b/.github/workflows/run-pgaftest.yml index c8b6c0300..99594c784 100644 --- a/.github/workflows/run-pgaftest.yml +++ b/.github/workflows/run-pgaftest.yml @@ -35,8 +35,16 @@ jobs: - name: Build pgaf:run image (PG${{ matrix.PGVERSION }}) run: | + # Match the CITUSTAG-per-PGVERSION logic from the top-level Makefile: + # PG14–15 require Citus v12 (last version supporting those releases). + # PG16+ use the current default. + case "${{ matrix.PGVERSION }}" in + 14|15) CITUSTAG=v12.1.5 ;; + *) CITUSTAG=v13.2.0 ;; + esac docker build \ --build-arg PGVERSION=${{ matrix.PGVERSION }} \ + --build-arg CITUSTAG=${CITUSTAG} \ --target run \ -t pgaf:run-pg${{ matrix.PGVERSION }} \ . @@ -74,6 +82,7 @@ jobs: run: | docker build \ --build-arg PGVERSION=${{ env.PGVERSION }} \ + --build-arg CITUSTAG=v13.2.0 \ --target build \ -t pgaf:build \ . diff --git a/Dockerfile b/Dockerfile index 32229a30b..4cfcd1bf7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -85,7 +85,7 @@ RUN echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers FROM base AS citus ARG PGVERSION -ARG CITUSTAG=v12.1.5 +ARG CITUSTAG=v13.2.0 ENV PG_CONFIG=/usr/lib/postgresql/${PGVERSION}/bin/pg_config From 2ec04eade290cabd5f432715c2b6e1dfd62ce2b8 Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Tue, 7 Jul 2026 14:08:08 +0200 Subject: [PATCH 27/68] ci: update GitHub Actions versions to Node 24-compatible releases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Align run-pgaftest.yml with the versions already used in run-tests.yml: actions/checkout v4.2.2 → v7.0.0 actions/upload-artifact v4.4.3 → v7.0.1 actions/download-artifact v4.1.8 → v8.0.1 --- .github/workflows/run-pgaftest.yml | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/run-pgaftest.yml b/.github/workflows/run-pgaftest.yml index 99594c784..a33fbb3c6 100644 --- a/.github/workflows/run-pgaftest.yml +++ b/.github/workflows/run-pgaftest.yml @@ -28,7 +28,7 @@ jobs: PGVERSION: [14, 15, 16, 17] steps: - - uses: actions/checkout@v4.2.2 + - uses: actions/checkout@v7.0.0 - name: Generate git-version.h run: make version @@ -55,7 +55,7 @@ jobs: | gzip > /tmp/pgaf-run-pg${{ matrix.PGVERSION }}.tar.gz - name: Upload image artifact - uses: actions/upload-artifact@v4.4.3 + uses: actions/upload-artifact@v7.0.1 with: name: pgaf-run-image-pg${{ matrix.PGVERSION }} path: /tmp/pgaf-run-pg${{ matrix.PGVERSION }}.tar.gz @@ -73,7 +73,7 @@ jobs: PGVERSION: 17 steps: - - uses: actions/checkout@v4.2.2 + - uses: actions/checkout@v7.0.0 - name: Generate git-version.h run: make version @@ -96,7 +96,7 @@ jobs: chmod +x /tmp/pgaftest-bin - name: Upload pgaftest binary - uses: actions/upload-artifact@v4.4.3 + uses: actions/upload-artifact@v7.0.1 with: name: pgaftest-binary path: /tmp/pgaftest-bin @@ -137,10 +137,10 @@ jobs: - extension_update steps: - - uses: actions/checkout@v4.2.2 + - uses: actions/checkout@v7.0.0 - name: Download pgaf:run image (PG${{ matrix.PGVERSION }}) - uses: actions/download-artifact@v4.1.8 + uses: actions/download-artifact@v8.0.1 with: name: pgaf-run-image-pg${{ matrix.PGVERSION }} path: /tmp @@ -152,7 +152,7 @@ jobs: docker tag pgaf:run-pg${{ matrix.PGVERSION }} pgaf:run - name: Download pgaftest binary - uses: actions/download-artifact@v4.1.8 + uses: actions/download-artifact@v8.0.1 with: name: pgaftest-binary path: /tmp @@ -192,7 +192,7 @@ jobs: PGVERSION: [14, 15, 16, 17] steps: - - uses: actions/checkout@v4.2.2 + - uses: actions/checkout@v7.0.0 - name: Install libpq runtime run: | @@ -206,7 +206,7 @@ jobs: sudo apt-get install -y libpq5 - name: Download pgaftest binary - uses: actions/download-artifact@v4.1.8 + uses: actions/download-artifact@v8.0.1 with: name: pgaftest-binary path: /tmp @@ -242,7 +242,7 @@ jobs: PGVERSION: 16 steps: - - uses: actions/checkout@v4.2.2 + - uses: actions/checkout@v7.0.0 with: # Full history needed so PREV_TAG auto-detection finds the right tag. fetch-depth: 0 @@ -259,7 +259,7 @@ jobs: sudo apt-get install -y libpq5 - name: Download pgaftest binary - uses: actions/download-artifact@v4.1.8 + uses: actions/download-artifact@v8.0.1 with: name: pgaftest-binary path: /tmp From c6036fee3c4fc2c107ad934ff7296ebf6087d1f0 Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Tue, 7 Jul 2026 14:49:23 +0200 Subject: [PATCH 28/68] Remove local-only planning file from PR --- .claude/pr-split-plan.md | 530 --------------------------------------- 1 file changed, 530 deletions(-) delete mode 100644 .claude/pr-split-plan.md diff --git a/.claude/pr-split-plan.md b/.claude/pr-split-plan.md deleted file mode 100644 index 60756a8a1..000000000 --- a/.claude/pr-split-plan.md +++ /dev/null @@ -1,530 +0,0 @@ -# PR Split Plan — pg_auto_failover #1125 - -Seven sequenced PRs extracted from the `pgaftest-infra` branch (48 commits, 158 files). -Each section is a self-contained brief for a new session. - -## Critical rebase note - -The commit history is NOT pre-split. The founding commit `a99e035` alone touches -common/, pg_autoctl commands, pgaftest, Dockerfile, CI, and specs simultaneously. -Later "fix CI" commits mix C source with spec changes in the same patch. - -**You cannot split by cherry-picking commits at existing boundaries.** -Each PR requires cherry-picking individual FILE changes out of mixed commits, -then squashing and cleaning up. Expect 2–4 hours of interactive rebase work -spread across sessions. - -Branch to split: `pgaftest-infra` -Base: `main` -Upstream: `hapostgres/pg_auto_failover` - ---- - -## PR 1 — CI modernisation & style check - -**Goal:** Update GitHub Actions dependencies and add an authoritative style-check job. -No source code changes. Merges first, unblocks everything else. - -### Files to change - -- `.github/workflows/run-tests.yml` -- `Makefile` (CI-related targets only: `citus_indent`, `banned` targets) -- `.gitattributes` (add `*.pgaf linguist-language=Ruby` or similar if present) -- `.gitignore` (add pgaftest build artefacts: `*.tab.c`, `*.tmp`) - -### What to do - -1. Create branch `ci-modernisation` off `main`. - -2. From `2a8efff` cherry-pick ONLY the `run-tests.yml` and `Makefile` CI changes: - ``` - git checkout pgaftest-infra -- .github/workflows/run-tests.yml - ``` - Then **revert** any lines that reference pgaftest-specific jobs - (those belong in PR 7's `run-pgaftest.yml`). - -3. Changes to make in `run-tests.yml`: - - `actions/checkout@v3` → `actions/checkout@v4.2.2` everywhere - - Remove the `PG14 linting` matrix entry - - Add a new `style_checker` job: - ```yaml - style_checker: - name: Style check - runs-on: ubuntu-latest - container: citus/stylechecker:no-py - steps: - - uses: actions/checkout@v4.2.2 - - run: git config --global --add safe.directory ${GITHUB_WORKSPACE} - - run: citus_indent --check - - run: ci/banned.h.sh - ``` - - Add PG17 to the PGVERSION matrix (was missing, only 13–16+18) - - Remove `TRAVIS_BUILD_DIR` env var export (dead code from old CI system) - -4. From `639913a` cherry-pick the `.gitignore` additions only. - -5. From `678c43e` / `d4acb65` cherry-pick `.gitattributes` if present. - -6. Verify: `git diff main HEAD --stat` should show only CI/meta files. - -### PR description - -"Update GitHub Actions to v4, add authoritative citus_indent style-check job -using `citus/stylechecker:no-py`, add PG17 to test matrix, remove TRAVIS_BUILD_DIR -remnants. No source changes." - ---- - -## PR 2 — Bug fixes in pg_autoctl (backportable) - -**Goal:** Ship the targeted C fixes independently. These fix real production bugs -and can be backported to v2.1. No structural changes. - -### Files to change - -- `src/bin/pg_autoctl/cli_common.c` -- `src/bin/pg_autoctl/cli_drop_node.c` + `file_utils.h` include -- `src/bin/pg_autoctl/cli_create_node.c` -- `src/bin/pg_autoctl/service_keeper.c` (minimal) -- `src/bin/pg_autoctl/fsm_transition.c` (if part of a173911) - -### What to do - -1. Create branch `fix-pg-autoctl-bugs` off `main`. - -2. Cherry-pick commit `7c093af` entirely (it's clean: SIGHUP fix, report_lsn timing, drop timeout): - ``` - git cherry-pick 7c093af - ``` - -3. Cherry-pick commit `afe91c3` entirely (drop-node state file): - ``` - git cherry-pick afe91c3 - ``` - -4. Cherry-pick commit `504c6b7` entirely (hba-lan pgSetup defer): - ``` - git cherry-pick 504c6b7 - ``` - -5. From `a173911` cherry-pick ONLY the C source changes (NOT Dockerfile or spec changes): - ``` - git checkout pgaftest-infra -- src/bin/pg_autoctl/fsm_transition.c - # verify it's only the upgrade build fix, not touching anything else - git diff HEAD - ``` - -6. Run style check: `docker run --rm -v $(pwd):/mnt citus/stylechecker:no-py citus_indent --check` - -### Tests to verify - -These fixes are validated by the existing Python test suite (`Run Tests` workflow). -The specific failing scenarios: -- SIGHUP fix: `test_basic_operation.py` reload step -- Drop-node fix: `test_basic_operation.py` drop secondary step -- hba-lan fix: any test that exercises `pg_autoctl create postgres --auth lan` - -### PR description - -"Three backportable bug fixes: -1. SIGHUP PID-reuse race in `pg_autoctl reload` — `signal(SIGHUP, SIG_IGN)` at - start of `cli_pg_autoctl_reload` prevents the broadcast SIGHUP from killing - a freshly exec'd one-shot command (exit 129). -2. Drop-node fails when service already cleaned up state file — add `else if` - branch in `cli_drop_local_node` that treats a missing state file as confirmation - of successful drop (pid ≠ 0 but state file gone). -3. hba-lan: defer pgSetup init until after config file exists." - ---- - -## PR 3 — Remove Azure integration - -**Goal:** Delete ~3 500 lines of unmaintained Azure scaffolding. Pure deletion, -no logic changes, unblocks the command reorganisation in PR 4. - -### Files to delete entirely - -``` -src/bin/pg_autoctl/azure.c -src/bin/pg_autoctl/azure.h -src/bin/pg_autoctl/azure_config.c -src/bin/pg_autoctl/azure_config.h -src/bin/pg_autoctl/cli_do_azure.c -src/bin/pg_autoctl/cli_do_tmux_azure.c -``` - -### Files to modify - -- `src/bin/pg_autoctl/cli_do_root.c` — remove azure subcommand registrations -- `src/bin/pg_autoctl/cli_do_root.h` — remove azure extern declarations -- `src/bin/pg_autoctl/Makefile` — remove azure*.c and cli_do_azure.c from OBJS - -### What to do - -1. Create branch `remove-azure` off `main` (or off PR 2 if that's merged first — - doesn't matter, no file overlap). - -2. Extract the azure deletions from `a99e035`: - ``` - git rm src/bin/pg_autoctl/azure.c src/bin/pg_autoctl/azure.h \ - src/bin/pg_autoctl/azure_config.c src/bin/pg_autoctl/azure_config.h \ - src/bin/pg_autoctl/cli_do_azure.c src/bin/pg_autoctl/cli_do_tmux_azure.c - ``` - -3. Edit `cli_do_root.c` to remove the azure extern and subcommand table entry. - Look for `azure_commands` and `cli_do_tmux_azure_commands` references. - -4. Edit `cli_do_root.h` to remove azure extern declarations. - -5. Edit `Makefile` to remove azure sources from `OBJS`. - -6. Build check: `make -C src/bin/pg_autoctl` should compile cleanly. - -### PR description - -"Remove unmaintained Azure integration (~3 500 lines). The `pg_autoctl do azure` -and `pg_autoctl do tmux azure` subcommands are deleted with no replacement — -the Azure tutorial was already broken and the code had no active users." - ---- - -## PR 4 — pg_autoctl command reorganisation (do → inspect / manual) - -**Goal:** Rename `do` to `internal` (stays hidden, subprocess-only entry points). -Add always-visible `inspect` (read-only diagnostics) and `manual` (FSM override ops) -subcommand trees. Update Python tests. Docs. - -**Depends on:** PR 3 merged (azure entries gone from `cli_do_root.c`). - -### New files - -- `src/bin/pg_autoctl/cli_inspect.c` + `.h` -- `src/bin/pg_autoctl/cli_manual.c` + `.h` - -### Modified files - -- `src/bin/pg_autoctl/cli_do_root.c` + `.h` -- `src/bin/pg_autoctl/cli_root.c` -- `src/bin/pg_autoctl/cli_do_fsm.c` (dispatch table name changes) -- `src/bin/pg_autoctl/cli_do_misc.c` -- `src/bin/pg_autoctl/cli_do_monitor.c` -- `src/bin/pg_autoctl/cli_do_service.c` -- `src/bin/pg_autoctl/cli_do_coordinator.c` -- `src/bin/pg_autoctl/cli_get_set_properties.c` (moved under inspect/manual) -- `docs/ref/pg_autoctl_do_pgsetup.rst` - -### What to do - -1. Create branch `pg-autoctl-commands` off PR 3 (or rebase onto main once PR 3 merges). - -2. The three relevant commits (`a99e035` command parts, `22ab675`, `bfd0dc4`, `21560e4`) - went through: `override` → `manual` → `internal` for the hidden tree. - The final state is: - - `pg_autoctl inspect …` — always visible, read-only diagnostics - - `pg_autoctl manual …` — always visible, FSM override operations - - `pg_autoctl internal …` — hidden, subprocess entry points for the supervisor - -3. Extract the command files from `pgaftest-infra`: - ``` - git checkout pgaftest-infra -- \ - src/bin/pg_autoctl/cli_inspect.c \ - src/bin/pg_autoctl/cli_inspect.h \ - src/bin/pg_autoctl/cli_manual.c \ - src/bin/pg_autoctl/cli_manual.h \ - src/bin/pg_autoctl/cli_do_root.c \ - src/bin/pg_autoctl/cli_do_root.h \ - src/bin/pg_autoctl/cli_root.c \ - src/bin/pg_autoctl/cli_do_fsm.c \ - src/bin/pg_autoctl/cli_do_misc.c \ - src/bin/pg_autoctl/cli_do_monitor.c \ - src/bin/pg_autoctl/cli_do_service.c \ - src/bin/pg_autoctl/cli_do_coordinator.c \ - src/bin/pg_autoctl/cli_get_set_properties.c \ - docs/ref/pg_autoctl_do_pgsetup.rst - ``` - Then manually verify each file: remove any pgaftest-specific references - (e.g. references to `pgaftest` in Makefile, any `cli_override` leftovers). - -4. Add `cli_inspect.c` and `cli_manual.c` to `src/bin/pg_autoctl/Makefile` OBJS. - -5. Run: `pg_autoctl --help` should show `inspect` and `manual` at top level. - `pg_autoctl internal --help` should be hidden (not in top-level help). - -6. Update Python tests: grep for `pg_autoctl do ` → `pg_autoctl inspect ` - or `pg_autoctl manual ` as appropriate. Commits `2a8efff` has the test changes. - -### PR description - -"Reorganise pg_autoctl command surface: -- `pg_autoctl inspect` (new, always visible): read-only diagnostics — wraps the - old `do show`, `do pgsetup`, `do monitor`, `do service getpid` subcommands -- `pg_autoctl manual` (new, always visible): FSM override operations — wraps the - old `do fsm assign/step`, `do primary`, `do standby`, `do coordinator` subcommands -- `pg_autoctl internal` (hidden): subprocess entry points for the supervisor - (was `do`, stays hidden, same purpose) -- `PG_AUTOCTL_DEBUG` no longer gates command visibility" - ---- - -## PR 5 — pg_autoctl node + node.ini - -**Goal:** New `pg_autoctl node` subcommand: declarative node spec file (`node.ini`) -and supervisor file-watcher that re-reads it on SIGHUP. - -**Depends on:** PR 4 merged (node hangs off the reorganised command tree). - -### New files - -- `src/bin/pg_autoctl/cli_node.c` + `.h` -- `src/bin/pg_autoctl/nodespec.c` + `.h` -- `docs/ref/pg_autoctl_node.rst` - -### Modified files - -- `src/bin/pg_autoctl/cli_root.c` (add node subcommand) -- `src/bin/pg_autoctl/keeper_config.c` + `.h` -- `src/bin/pg_autoctl/supervisor.c` + `.h` (SIGHUP handler) -- `src/bin/pg_autoctl/Makefile` (add cli_node.o, nodespec.o) -- `docs/index.rst` - -### What to do - -1. Create branch `pg-autoctl-node` off PR 4 (or rebase once PR 4 merges). - -2. Extract from `pgaftest-infra`: - ``` - git checkout pgaftest-infra -- \ - src/bin/pg_autoctl/cli_node.c \ - src/bin/pg_autoctl/cli_node.h \ - src/bin/pg_autoctl/nodespec.c \ - src/bin/pg_autoctl/nodespec.h \ - docs/ref/pg_autoctl_node.rst \ - docs/index.rst - ``` - -3. From `fe341da` extract the supervisor.c SIGHUP watcher changes and keeper_config changes. - Do NOT take the pgaftest-related parts of `c896747`. - -4. Add `cli_node.o nodespec.o` to `src/bin/pg_autoctl/Makefile`. - -5. Register `node_commands` in `cli_root.c` (visible, no PG_AUTOCTL_DEBUG gate). - -6. Verify: `pg_autoctl node --help` shows `run`, `show`, `edit` subcommands. - `pg_autoctl node run --pgdata /tmp/test` should read `node.ini` from PGDATA. - -### Note on optional deferral - -This PR can be deferred or kept merged with PR 4 if the node.ini work needs -more iteration. PR 4 is a self-contained rename and is shippable without PR 5. - -### PR description - -"Add `pg_autoctl node` subcommand tree: declarative node spec file (node.ini) -lets operators manage node configuration as a versioned file rather than a -sequence of `pg_autoctl set` commands. The supervisor file-watcher re-reads -node.ini on SIGHUP, applying changes without a full restart." - ---- - -## PR 6 — src/bin/common/ shared library build - -**Goal:** Move ~20 generic utility source files out of `src/bin/pg_autoctl/` into -`src/bin/common/` (a new static library). Both `pg_autoctl` and `pgaftest` link it. -Pure mechanical file moves — no logic changes. - -**Depends on:** PR 5 merged (pg_autoctl Makefile fully settled before adding common/). - -### Files being moved (git rename — no content changes) - -``` -debian.c/h env_utils.c/h file_utils.c/h -ini_file.c/h ini_implementation.c -ipaddr.c/h lock_utils.c/h -parsing.c/h pgctl.c/h pgsetup.c/h -pgsql.c/h pgtuning.c/h pidfile.c/h -signals.c/h string_utils.c/h system_utils.c/h -``` - -### New files - -- `src/bin/common/Makefile` -- `src/bin/common/Makefile.common` - -### Modified files - -- `src/bin/pg_autoctl/Makefile` — point to `../common/` for moved files, - link `../common/libpgaf_common.a` -- `src/bin/Makefile` — add `common` as first build target -- `src/bin/pgaftest/Makefile` — link `../common/libpgaf_common.a` - -### What to do - -1. Create branch `common-library` off PR 5 (or off main once PRs 1–5 merge). - -2. Cherry-pick `3d42128`: - ``` - git cherry-pick 3d42128 - ``` - This commit IS the file moves; git detects renames automatically. - But it also touches `pgaftest/Makefile` — that's OK since pgaftest build - references are inert until PR 7 adds the pgaftest source files. - -3. Cherry-pick `cabeb1e` (macOS build fix): - ``` - git cherry-pick cabeb1e - ``` - -4. Cherry-pick `b0d52e0` (dangling pointer warnings): - ``` - git cherry-pick b0d52e0 - ``` - -5. Verify pg_autoctl still builds: - ``` - make -C src/bin clean && make -C src/bin - ``` - The pg_autoctl binary should compile with headers resolved from `../common`. - -6. Check that `#include` paths in pg_autoctl source still resolve (they use - bare names like `#include "file_utils.h"` — the Makefile `-I ../common` flag - makes this work). - -### Rebase risk - -The `src/bin/pg_autoctl/Makefile` is heavily modified by PRs 3–5. -Rebase `common-library` on top of the merged PRs, resolve Makefile conflicts once. -The moved files themselves will not conflict (git rename detection is reliable here). - -### PR description - -"Move generic utility sources to src/bin/common/ static library. Both -`pg_autoctl` and `pgaftest` link against it. No logic changes — pure file -reorganisation to enable the pgaftest binary (PR 7) without duplicating sources." - ---- - -## PR 7 — pgaftest binary, DSL, test suite, upgrade test, CI workflow - -**Goal:** Everything pgaftest. The new `pgaftest` binary with its bison/flex DSL, -22 `.pgaf` spec files covering all Python test scenarios, the live-upgrade test -with the dual-binary Docker image, and the `run-pgaftest.yml` CI workflow. - -**Depends on:** PR 1 (CI workflow extends updated run-tests.yml) + PR 6 (pgaftest -links against src/bin/common/). - -### New files - -**Binary source** (`src/bin/pgaftest/`): -- `main.c`, `cli_root.c` — pgaftest CLI dispatch -- `test_spec.h` — AST node structs -- `test_spec_scan.l` — Flex lexer -- `test_spec_parse.y` — Bison grammar (pre-generated `.c/.h` files committed) -- `test_spec_parse.c/h`, `test_spec_scan.c` — committed bison/flex output -- `test_runner.c/h` — run/setup/step/down logic, TAP output, LISTEN loop -- `compose_gen.c/h` — Docker Compose YAML generator from cluster{} block -- `cli_demo.c/h` — demo app (moved from pg_autoctl do demo) -- `Makefile` - -**Spec files** (`tests/tap/specs/`): 22 `.pgaf` files + `tests/tap/schedule` - -**Upgrade test** (`tests/upgrade/`): -- `Dockerfile.current` — dual-binary image (v2.1 + v2.2) -- `Makefile` — `pgaf-current` and `pgaf-next` targets -- `install-extension.sh` — extension file installer baked into image -- `pg_autoctl_shim.sh` — translates `do service` → `internal service` after symlink flip - -**CI workflow**: `.github/workflows/run-pgaftest.yml` - -**Dockerfile**: add `pgaftest` and `debian` multi-stage targets, guard bison touch step - -**Docs**: `docs/pgaftest.rst` (1 071 lines), `docs/ref/manual.rst` update - -### What to do - -1. Create branch `pgaftest` off PR 6 (or off main once PRs 1–6 merge). - -2. The cleanest approach: take the full pgaftest-infra branch, then remove - everything that belongs in PRs 1–6, leaving only pgaftest-specific changes: - ``` - git checkout pgaftest-infra - git rebase -i main # squash all 48 commits into ~8 thematic ones - ``` - Then rebase the result onto PR 6. - -3. **Squash strategy** — 8 clean commits for this PR: - - `pgaftest: build system and Makefile` — Makefile + common/ link - - `pgaftest: DSL — lexer, parser, AST` — .l, .y, generated .c/.h, test_spec.h - - `pgaftest: runner and CLI` — test_runner.c, cli_root.c, compose_gen.c, main.c - - `pgaftest: spec suite` — all 22 .pgaf files + schedule - - `pgaftest: upgrade test` — tests/upgrade/ additions - - `pgaftest: Dockerfile targets` — pgaftest + debian stages, bison touch guard - - `pgaftest: GitHub Actions workflow` — run-pgaftest.yml - - `pgaftest: docs` — pgaftest.rst, manual.rst - -4. **Bison touch guard** (important): the Dockerfile must touch pre-generated - files BEFORE `make` so bison doesn't regenerate them (CI bison version may differ). - Current correct form: - ```dockerfile - RUN if [ -d src/bin/pgaftest ]; then \ - touch src/bin/pgaftest/test_spec_parse.c \ - src/bin/pgaftest/test_spec_parse.h \ - src/bin/pgaftest/test_spec_scan.c; \ - fi - ``` - The `-d` guard is needed because the upgrade test builds from a `git archive v2.1` - that does not have `src/bin/pgaftest/` at all. - -5. **Upgrade test image build**: before the CI run, the upgrade images must be built: - ``` - make -C tests/upgrade pgaf-current # builds pgaf:current (v2.1 + v2.2 binaries) - make -C tests/upgrade pgaf-next # builds pgaf:next (current branch) - ``` - This is automated in the `run-pgaftest.yml` workflow under the `upgrade` job. - -6. Verify the full pgaftest suite locally (PG17 default): - ``` - make pgaftest-image # build pgaftest Docker image - pgaftest run tests/tap/specs/basic_operation.pgaf - pgaftest run tests/tap/specs/multi_async.pgaf - ``` - -7. CI target: `pgaftest workflow` should show 22/22 green. The known pre-existing - failures (PG18, flaky Python tests) are in `Run Tests`, not here. - -### Key bugs already fixed (do NOT regress) - -These are in the current `pgaftest-infra` branch and must be carried into PR 7: -- `c3ae322` — trust notify convergence without subprocess double-check (multi_async fix) -- `5aba400` — poll monitor directly when LISTEN notifications missed (upgrade fix) -- `2185732` / `518d38f` — Dockerfile bison touch explicit paths + guard -- `7c093af` — SIGHUP race (already in PR 2; don't duplicate, just ensure it's in main first) - -### PR description - -"Add pgaftest: a new test binary and `.pgaf` spec language for deterministic -cluster testing. Replaces the Python nose test suite for CI, while also supporting -interactive cluster setup (`pgaftest setup spec.pgaf`). - -- 22 spec files covering all current Python test scenarios -- Live upgrade test: binary + extension swap without container restart -- GitHub Actions workflow: 22 parallel jobs, each spec in its own Docker Compose network -- TAP output: compatible with `prove` and standard CI TAP consumers" - ---- - -## Merge order summary - -``` -main ──┬── PR 1 (CI) ──────────────────────────────────────────┐ - └── PR 2 (bug fixes) │ - └── PR 3 (remove azure) ─────────────────────────────────┤ - └── PR 4 (commands) ─────────────────────────────── ┤ - └── PR 5 (node) ───────────────────────────── ┤ - └── PR 6 (common/) ─────────────────────┤ - └── PR 7 (pgaftest) ◄───────┘ - (also needs PR 1) -``` - -PRs 1 and 2 have no dependencies and can be opened simultaneously. -PR 3 can open once PR 1's style check passes (or in parallel — no file overlap). -PRs 4 → 5 → 6 → 7 are a linear chain. From 5d604b44113f3fcb399718d2c4a3e6caf0268fb8 Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Tue, 7 Jul 2026 15:07:57 +0200 Subject: [PATCH 29/68] Fix CI failures: missing pg_autoctl options and installcheck CITUSTAG MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five root causes fixed: 1. pg_autoctl inspect pgsetup hba-lan — unknown command Add hba-lan to do_pgsetup[] subcommand array and define do_pgsetup_hba_lan CommandLine entry in cli_do_root.c. 2. pg_autoctl inspect pgsetup wait --timeout N — unrecognized option Add a dedicated keeper_cli_pgsetup_wait_getopts() that accepts --pgdata and --timeout. Default timeout remains 30 s; any positive integer can be supplied via --timeout. 3. pg_autoctl drop node --no-wait — unrecognized option Add --no-wait to cli_drop_node_getopts(). Sets listen_notifications_timeout to 0, which the existing code already treats as 'skip waiting'. 4. pg_autoctl create postgres/monitor reject --replication-password, --monitor-password, --autoctl-node-password generated by pg_autoctl node run from a nodespec.ini with passwords configured, causing container auth-node1-1 to exit(1): - Add --replication-password / --monitor-password to cli_create_postgres_getopts via cli_common_keeper_getopts ('e'/'w'). replication_password is stored in KeeperConfig (already used when creating pgautofailover_replicator); monitor-password is accepted and ignored (health-check role uses a hardcoded password for now). - Add --autoctl-node-password to cli_create_monitor_getopts ('e'). Stored in the new MonitorConfig.autoctl_node_password field and written to the ini file. monitor_install() accepts the password as a new parameter and calls pgsql_alter_role_password() on autoctl_node after extension creation when non-empty. 5. installcheck PG14 Citus incompatibility The installcheck job was building --target testrun without passing CITUSTAG, so the Dockerfile default v13.2.0 was used — incompatible with PG14. Add the same case/esac per-version CITUSTAG logic that build-images already uses. --- .github/workflows/run-pgaftest.yml | 5 ++ src/bin/pg_autoctl/cli_common.c | 22 +++++ src/bin/pg_autoctl/cli_create_node.c | 15 +++- src/bin/pg_autoctl/cli_do_misc.c | 100 ++++++++++++++++++++-- src/bin/pg_autoctl/cli_do_root.c | 12 ++- src/bin/pg_autoctl/cli_do_root.h | 1 + src/bin/pg_autoctl/cli_drop_node.c | 12 ++- src/bin/pg_autoctl/monitor_config.c | 3 + src/bin/pg_autoctl/monitor_config.h | 3 + src/bin/pg_autoctl/monitor_pg_init.c | 15 +++- src/bin/pg_autoctl/monitor_pg_init.h | 3 +- src/bin/pg_autoctl/service_monitor_init.c | 3 +- 12 files changed, 182 insertions(+), 12 deletions(-) diff --git a/.github/workflows/run-pgaftest.yml b/.github/workflows/run-pgaftest.yml index a33fbb3c6..097eae6d7 100644 --- a/.github/workflows/run-pgaftest.yml +++ b/.github/workflows/run-pgaftest.yml @@ -216,8 +216,13 @@ jobs: - name: Build pgaf:testrun image (PG${{ matrix.PGVERSION }}) run: | + case "${{ matrix.PGVERSION }}" in + 14|15) CITUSTAG=v12.1.5 ;; + *) CITUSTAG=v13.2.0 ;; + esac docker build \ --build-arg PGVERSION=${{ matrix.PGVERSION }} \ + --build-arg CITUSTAG=${CITUSTAG} \ --target testrun \ -t pgaf:testrun \ . diff --git a/src/bin/pg_autoctl/cli_common.c b/src/bin/pg_autoctl/cli_common.c index 4b835ab28..794c00ae8 100644 --- a/src/bin/pg_autoctl/cli_common.c +++ b/src/bin/pg_autoctl/cli_common.c @@ -407,6 +407,28 @@ cli_common_keeper_getopts(int argc, char **argv, break; } + case 'e': + { + /* { "replication-password", required_argument, NULL, 'e' } */ + strlcpy(LocalOptionConfig.replication_password, optarg, + MAXCONNINFO); + log_trace("--replication-password ****"); + break; + } + + case 'w': + { + /* + * { "monitor-password", required_argument, NULL, 'w' } + * The pgautofailover_monitor health-check role currently uses a + * hardcoded password (PG_AUTOCTL_HEALTH_PASSWORD). Accept the + * option so pg_autoctl node run can pass it without error; it + * is otherwise unused at this time. + */ + log_trace("--monitor-password ****"); + break; + } + case 'V': { /* keeper_cli_print_version prints version and exits. */ diff --git a/src/bin/pg_autoctl/cli_create_node.c b/src/bin/pg_autoctl/cli_create_node.c index 8ef2e8643..4b870c218 100644 --- a/src/bin/pg_autoctl/cli_create_node.c +++ b/src/bin/pg_autoctl/cli_create_node.c @@ -341,6 +341,8 @@ cli_create_postgres_getopts(int argc, char **argv) { "candidate-priority", required_argument, NULL, 'P' }, { "replication-quorum", required_argument, NULL, 'r' }, { "maximum-backup-rate", required_argument, NULL, 'R' }, + { "replication-password", required_argument, NULL, 'e' }, + { "monitor-password", required_argument, NULL, 'w' }, { "run", no_argument, NULL, 'x' }, { "no-ssl", no_argument, NULL, 'N' }, { "ssl-self-signed", no_argument, NULL, 's' }, @@ -354,7 +356,7 @@ cli_create_postgres_getopts(int argc, char **argv) int optind = cli_create_node_getopts(argc, argv, long_options, - "C:D:H:p:l:U:A:SLd:a:n:f:m:MI:RVvqhP:r:xsN", + "C:D:H:p:l:U:A:SLd:a:n:f:m:MI:Re:w:VvqhP:r:xsN", &options); /* publish our option parsing in the global variable */ @@ -793,6 +795,7 @@ cli_create_monitor_getopts(int argc, char **argv) { "listen", required_argument, NULL, 'l' }, { "auth", required_argument, NULL, 'A' }, { "skip-pg-hba", no_argument, NULL, 'S' }, + { "autoctl-node-password", required_argument, NULL, 'e' }, { "version", no_argument, NULL, 'V' }, { "verbose", no_argument, NULL, 'v' }, { "quiet", no_argument, NULL, 'q' }, @@ -821,7 +824,7 @@ cli_create_monitor_getopts(int argc, char **argv) optind = 0; - while ((c = getopt_long(argc, argv, "C:D:p:n:l:A:SVvqhxNs", + while ((c = getopt_long(argc, argv, "C:D:p:n:l:A:Se:VvqhxNs", long_options, &option_index)) != -1) { switch (c) @@ -898,6 +901,14 @@ cli_create_monitor_getopts(int argc, char **argv) break; } + case 'e': + { + /* { "autoctl-node-password", required_argument, NULL, 'e' } */ + strlcpy(options.autoctl_node_password, optarg, MAXCONNINFO); + log_trace("--autoctl-node-password ****"); + break; + } + case 'V': { /* keeper_cli_print_version prints version and exits. */ diff --git a/src/bin/pg_autoctl/cli_do_misc.c b/src/bin/pg_autoctl/cli_do_misc.c index 188faa88f..73e3cafb7 100644 --- a/src/bin/pg_autoctl/cli_do_misc.c +++ b/src/bin/pg_autoctl/cli_do_misc.c @@ -322,15 +322,104 @@ keeper_cli_pgsetup_is_ready(int argc, char **argv) } +/* timeout parsed by keeper_cli_pgsetup_wait_getopts, consumed by wait_until_ready */ +static int pgsetup_wait_timeout = 30; + /* - * keeper_cli_discover_pg_setup implements the CLI to discover a PostgreSQL - * setup thanks to PGDATA and other environment variables. + * keeper_cli_pgsetup_wait_getopts parses --pgdata and --timeout for the + * "pgsetup wait" subcommand. + */ +int +keeper_cli_pgsetup_wait_getopts(int argc, char **argv) +{ + int c, option_index = 0; + + static struct option long_options[] = { + { "pgdata", required_argument, NULL, 'D' }, + { "timeout", required_argument, NULL, 't' }, + { "version", no_argument, NULL, 'V' }, + { "verbose", no_argument, NULL, 'v' }, + { "quiet", no_argument, NULL, 'q' }, + { "help", no_argument, NULL, 'h' }, + { NULL, 0, NULL, 0 } + }; + + optind = 0; + + while ((c = getopt_long(argc, argv, "D:t:Vvqh", + long_options, &option_index)) != -1) + { + switch (c) + { + case 'D': + { + strlcpy(keeperOptions.pgSetup.pgdata, optarg, MAXPGPATH); + log_trace("--pgdata %s", optarg); + break; + } + + case 't': + { + if (!stringToInt(optarg, &pgsetup_wait_timeout) || + pgsetup_wait_timeout <= 0) + { + log_fatal("--timeout argument is not a valid positive integer: \"%s\"", + optarg); + exit(EXIT_CODE_BAD_ARGS); + } + log_trace("--timeout %d", pgsetup_wait_timeout); + break; + } + + case 'V': + { + keeper_cli_print_version(argc, argv); + break; + } + + case 'v': + { + log_set_level(LOG_DEBUG); + break; + } + + case 'q': + { + log_set_level(LOG_ERROR); + break; + } + + case 'h': + { + commandline_help(stderr); + exit(EXIT_CODE_QUIT); + break; + } + + default: + { + commandline_help(stderr); + exit(EXIT_CODE_BAD_ARGS); + break; + } + } + } + + /* publish parsed options */ + keeperOptions.pgSetup.pgdata[0] = + keeperOptions.pgSetup.pgdata[0]; /* no-op, already set above */ + + return optind; +} + + +/* + * keeper_cli_pgsetup_wait_until_ready waits until the local Postgres server + * is ready to accept connections, up to --timeout seconds (default 30). */ void keeper_cli_pgsetup_wait_until_ready(int argc, char **argv) { - int timeout = 30; - ConfigFilePaths pathnames = { 0 }; LocalPostgresServer postgres = { 0 }; PostgresSetup *pgSetup = &(postgres.postgresSetup); @@ -343,7 +432,8 @@ keeper_cli_pgsetup_wait_until_ready(int argc, char **argv) log_debug("Initialized pgSetup, now calling pg_setup_wait_until_is_ready()"); - bool pgIsReady = pg_setup_wait_until_is_ready(pgSetup, timeout, LOG_INFO); + bool pgIsReady = + pg_setup_wait_until_is_ready(pgSetup, pgsetup_wait_timeout, LOG_INFO); log_info("Postgres status is: \"%s\"", pmStatusToString(pgSetup->pm_status)); diff --git a/src/bin/pg_autoctl/cli_do_root.c b/src/bin/pg_autoctl/cli_do_root.c index a7f6a86f8..9ee0752c7 100644 --- a/src/bin/pg_autoctl/cli_do_root.c +++ b/src/bin/pg_autoctl/cli_do_root.c @@ -186,9 +186,18 @@ CommandLine do_pgsetup_wait_until_ready = make_command("wait", "Wait until the local Postgres server is ready", "[option ...]", + " --pgdata path to data directory\n" + " --timeout seconds to wait, default 30\n", + keeper_cli_pgsetup_wait_getopts, + keeper_cli_pgsetup_wait_until_ready); + +CommandLine do_pgsetup_hba_lan = + make_command("hba-lan", + "Add LAN CIDR trust rules to pg_hba.conf and reload", + "[option ...]", KEEPER_CLI_WORKER_SETUP_OPTIONS, keeper_cli_keeper_setup_getopts, - keeper_cli_pgsetup_wait_until_ready); + keeper_cli_pgsetup_hba_lan); CommandLine do_pgsetup_startup_logs = make_command("logs", @@ -213,6 +222,7 @@ CommandLine *do_pgsetup[] = { &do_pgsetup_wait_until_ready, &do_pgsetup_startup_logs, &do_pgsetup_tune, + &do_pgsetup_hba_lan, NULL }; diff --git a/src/bin/pg_autoctl/cli_do_root.h b/src/bin/pg_autoctl/cli_do_root.h index aa60d3070..732c486da 100644 --- a/src/bin/pg_autoctl/cli_do_root.h +++ b/src/bin/pg_autoctl/cli_do_root.h @@ -101,6 +101,7 @@ void keeper_cli_pgsetup_pg_ctl(int argc, char **argv); void keeper_cli_pgsetup_hba_lan(int argc, char **argv); void keeper_cli_pgsetup_discover(int argc, char **argv); void keeper_cli_pgsetup_is_ready(int argc, char **argv); +int keeper_cli_pgsetup_wait_getopts(int argc, char **argv); void keeper_cli_pgsetup_wait_until_ready(int argc, char **argv); void keeper_cli_pgsetup_startup_logs(int argc, char **argv); void keeper_cli_pgsetup_tune(int argc, char **argv); diff --git a/src/bin/pg_autoctl/cli_drop_node.c b/src/bin/pg_autoctl/cli_drop_node.c index 0dc4f5ab6..a16e52a6f 100644 --- a/src/bin/pg_autoctl/cli_drop_node.c +++ b/src/bin/pg_autoctl/cli_drop_node.c @@ -84,7 +84,8 @@ CommandLine drop_node_command = " --pgport drop the node with given hostname and pgport\n" " --destroy also destroy Postgres database\n" " --force force dropping the node from the monitor\n" - " --wait how many seconds to wait, default to 60 \n", + " --wait how many seconds to wait, default to 60\n" + " --no-wait drop the node without waiting for confirmation\n", cli_drop_node_getopts, cli_drop_node); @@ -104,6 +105,7 @@ cli_drop_node_getopts(int argc, char **argv) { "monitor", required_argument, NULL, 'm' }, { "destroy", no_argument, NULL, 'd' }, { "force", no_argument, NULL, 'F' }, + { "no-wait", no_argument, NULL, 'W' }, { "hostname", required_argument, NULL, 'n' }, { "pgport", required_argument, NULL, 'p' }, { "formation", required_argument, NULL, 'f' }, @@ -160,6 +162,14 @@ cli_drop_node_getopts(int argc, char **argv) break; } + case 'W': + { + /* --no-wait: set timeout to zero so the notification loop is skipped */ + options.listen_notifications_timeout = 0; + log_trace("--no-wait"); + break; + } + case 'n': { strlcpy(options.hostname, optarg, _POSIX_HOST_NAME_MAX); diff --git a/src/bin/pg_autoctl/monitor_config.c b/src/bin/pg_autoctl/monitor_config.c index 77021471e..f62be0d8b 100644 --- a/src/bin/pg_autoctl/monitor_config.c +++ b/src/bin/pg_autoctl/monitor_config.c @@ -118,6 +118,9 @@ OPTION_SSL_CRL_FILE(config), \ OPTION_SSL_SERVER_CERT(config), \ OPTION_SSL_SERVER_KEY(config), \ + make_strbuf_option_default("pg_auto_failover", "autoctl_node_password", \ + NULL, false, MAXCONNINFO, \ + config->autoctl_node_password, ""), \ INI_OPTION_LAST \ } diff --git a/src/bin/pg_autoctl/monitor_config.h b/src/bin/pg_autoctl/monitor_config.h index c7292d992..b30a8ff8d 100644 --- a/src/bin/pg_autoctl/monitor_config.h +++ b/src/bin/pg_autoctl/monitor_config.h @@ -31,6 +31,9 @@ typedef struct MonitorConfig /* PostgreSQL setup */ PostgresSetup pgSetup; + + /* password for the autoctl_node role; "" means no password (trust) */ + char autoctl_node_password[MAXCONNINFO]; } MonitorConfig; diff --git a/src/bin/pg_autoctl/monitor_pg_init.c b/src/bin/pg_autoctl/monitor_pg_init.c index 2835c7f3a..6f99e2e8c 100644 --- a/src/bin/pg_autoctl/monitor_pg_init.c +++ b/src/bin/pg_autoctl/monitor_pg_init.c @@ -139,7 +139,8 @@ monitor_pg_init(Monitor *monitor) */ bool monitor_install(const char *hostname, - PostgresSetup pgSetupOption, bool checkSettings) + PostgresSetup pgSetupOption, bool checkSettings, + const char *autoctl_node_password) { PostgresSetup pgSetup = { 0 }; bool missingPgdataIsOk = false; @@ -249,6 +250,18 @@ monitor_install(const char *hostname, return false; } + if (autoctl_node_password != NULL && autoctl_node_password[0] != '\0') + { + if (!pgsql_alter_role_password(&postgres.sqlClient, + PG_AUTOCTL_MONITOR_USERNAME, + autoctl_node_password)) + { + log_error("Failed to set password for role \"%s\"", + PG_AUTOCTL_MONITOR_USERNAME); + return false; + } + } + log_info("Your pg_auto_failover monitor instance is now ready on port %d.", pgSetup.pgport); diff --git a/src/bin/pg_autoctl/monitor_pg_init.h b/src/bin/pg_autoctl/monitor_pg_init.h index 38a552c65..aed2b521e 100644 --- a/src/bin/pg_autoctl/monitor_pg_init.h +++ b/src/bin/pg_autoctl/monitor_pg_init.h @@ -18,7 +18,8 @@ bool monitor_pg_init(Monitor *monitor); bool monitor_install(const char *hostname, PostgresSetup pgSetupOption, - bool checkSettings); + bool checkSettings, + const char *autoctl_node_password); bool monitor_add_postgres_default_settings(Monitor *monitor); #endif /* MONITOR_PG_INIT_H */ diff --git a/src/bin/pg_autoctl/service_monitor_init.c b/src/bin/pg_autoctl/service_monitor_init.c index 06998b43c..38de1aad2 100644 --- a/src/bin/pg_autoctl/service_monitor_init.c +++ b/src/bin/pg_autoctl/service_monitor_init.c @@ -134,7 +134,8 @@ service_monitor_init_start(void *context, pid_t *pid) (void) set_ps_title(serviceName); /* finish the install if necessary */ - if (!monitor_install(config->hostname, *pgSetup, false)) + if (!monitor_install(config->hostname, *pgSetup, false, + config->autoctl_node_password)) { /* errors have already been logged */ exit(EXIT_CODE_INTERNAL_ERROR); From 4f1b859059d4ae3ba465073795674493e9cba776 Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Tue, 7 Jul 2026 15:17:36 +0200 Subject: [PATCH 30/68] tests: fix spec timeouts and pg_ctl path for CI runners Increase several wait-until timeouts from 120s to 180s in specs that were hitting the limit on GitHub Actions shared runners: - ssl_cert: setup waits longer for SSL cert auth cluster to form - ensure: test_004_demoted failover wait - multi_async: test_011 report_lsn wait for node4 - multi_alternate: test_005_003 demoted/secondary wait for node2 - multi_ifdown: test_008_failover wait_primary/primary wait for node3 Also fix config_get_set: replace hardcoded /usr/lib/postgresql/17/bin/pg_ctl with /bin/pg_ctl since expect{} does substring matching and the path contains the actual PGVERSION, not always 17. --- tests/tap/specs/config_get_set.pgaf | 8 ++++---- tests/tap/specs/ensure.pgaf | 4 ++-- tests/tap/specs/multi_alternate.pgaf | 4 ++-- tests/tap/specs/multi_async.pgaf | 4 ++-- tests/tap/specs/multi_ifdown.pgaf | 4 ++-- tests/tap/specs/ssl_cert.pgaf | 2 +- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/tap/specs/config_get_set.pgaf b/tests/tap/specs/config_get_set.pgaf index 0374e05e9..f1d83fb48 100644 --- a/tests/tap/specs/config_get_set.pgaf +++ b/tests/tap/specs/config_get_set.pgaf @@ -31,18 +31,18 @@ step test_001_init_primary { step test_002_config_set_monitor { exec monitor pg_autoctl config set ssl.sslmode prefer exec monitor pg_autoctl config get postgresql.pg_ctl - expect { /usr/lib/postgresql/17/bin/pg_ctl } + expect { /bin/pg_ctl } exec-fails monitor pg_autoctl config set postgresql.pg_ctl invalid exec monitor pg_autoctl config get postgresql.pg_ctl - expect { /usr/lib/postgresql/17/bin/pg_ctl } + expect { /bin/pg_ctl } exec monitor pg_autoctl config get ssl.sslmode expect { prefer } } step test_002b_config_set_node { exec node1 pg_autoctl config get postgresql.pg_ctl - expect { /usr/lib/postgresql/17/bin/pg_ctl } + expect { /bin/pg_ctl } exec-fails node1 pg_autoctl config set postgresql.pg_ctl invalid exec node1 pg_autoctl config get postgresql.pg_ctl - expect { /usr/lib/postgresql/17/bin/pg_ctl } + expect { /bin/pg_ctl } } diff --git a/tests/tap/specs/ensure.pgaf b/tests/tap/specs/ensure.pgaf index 9020fa7a9..1e24ed3a3 100644 --- a/tests/tap/specs/ensure.pgaf +++ b/tests/tap/specs/ensure.pgaf @@ -51,10 +51,10 @@ step test_004_demoted { compose stop node1 sleep 30s compose start node1 - wait until node1 state is demoted timeout 120s + wait until node1 state is demoted timeout 180s wait until node2 state is primary and node1 state is secondary - timeout 120s + timeout 180s } step test_005_inject_error_in_node2 { diff --git a/tests/tap/specs/multi_alternate.pgaf b/tests/tap/specs/multi_alternate.pgaf index 5d1ba9d9c..2855fdcee 100644 --- a/tests/tap/specs/multi_alternate.pgaf +++ b/tests/tap/specs/multi_alternate.pgaf @@ -161,10 +161,10 @@ step test_005_002_fail_primary_again { step test_005_003_bring_up_first_failed_primary { compose start node2 - wait until node2 state is demoted timeout 120s + wait until node2 state is demoted timeout 180s wait until node2 state is secondary and node3 state is primary - timeout 120s + timeout 180s } step test_005_004_bring_up_last_failed_primary { diff --git a/tests/tap/specs/multi_async.pgaf b/tests/tap/specs/multi_async.pgaf index d705d6fcb..cfbef8dbd 100644 --- a/tests/tap/specs/multi_async.pgaf +++ b/tests/tap/specs/multi_async.pgaf @@ -110,9 +110,9 @@ step test_010_promote_node1 { # step test_011_ifdown_node4_at_reportlsn { - wait until node4 state is report_lsn timeout 120s + wait until node4 state is report_lsn timeout 180s network disconnect node4 - wait until node3 state is secondary timeout 120s + wait until node3 state is secondary timeout 180s } # diff --git a/tests/tap/specs/multi_ifdown.pgaf b/tests/tap/specs/multi_ifdown.pgaf index a3f1d0417..bee3c057a 100644 --- a/tests/tap/specs/multi_ifdown.pgaf +++ b/tests/tap/specs/multi_ifdown.pgaf @@ -89,10 +89,10 @@ step test_007_insert_rows { step test_008_failover { network disconnect node1 network connect node3 - wait until node3 state is wait_primary timeout 120s + wait until node3 state is wait_primary timeout 180s wait until node2 state is secondary and node3 state is primary - timeout 120s + timeout 180s sql node3 { SHOW synchronous_standby_names; } expect { ANY 1 (pgautofailover_standby_1, pgautofailover_standby_2) } } diff --git a/tests/tap/specs/ssl_cert.pgaf b/tests/tap/specs/ssl_cert.pgaf index aab711be7..7a49d6328 100644 --- a/tests/tap/specs/ssl_cert.pgaf +++ b/tests/tap/specs/ssl_cert.pgaf @@ -21,7 +21,7 @@ cluster { setup { wait until node1 state is primary and node2 state is secondary - timeout 120s + timeout 180s promote node1 } From 7e0c8079555f9094ef05d6a8d2415790937d3a91 Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Tue, 7 Jul 2026 15:23:13 +0200 Subject: [PATCH 31/68] style: apply citus_indent formatting --- src/bin/common/ini_file.h | 30 +- src/bin/common/pgctl.c | 2 +- src/bin/common/pgsetup.c | 2 +- src/bin/common/pgsetup.h | 4 +- src/bin/common/pgsql.c | 2 +- src/bin/common/pgsql.h | 38 +- src/bin/pg_autoctl/cli_common.h | 70 +- src/bin/pg_autoctl/cli_do_misc.c | 5 +- src/bin/pg_autoctl/cli_do_root.h | 2 +- src/bin/pg_autoctl/config.c | 3 +- src/bin/pg_autoctl/config.h | 2 +- src/bin/pg_autoctl/demoapp.c | 4 +- src/bin/pg_autoctl/fsm.c | 924 ++++++---- src/bin/pg_autoctl/keeper.c | 15 +- src/bin/pg_autoctl/keeper_config.c | 298 ++-- src/bin/pg_autoctl/keeper_config.h | 2 +- src/bin/pg_autoctl/keeper_pg_init.c | 3 +- src/bin/pg_autoctl/monitor_config.c | 118 +- src/bin/pg_autoctl/nodespec.c | 10 +- src/bin/pg_autoctl/primary_standby.c | 58 +- src/bin/pg_autoctl/service_monitor_init.c | 2 +- src/bin/pg_autoctl/state.c | 4 + src/bin/pg_autoctl/systemd_config.c | 66 +- src/bin/pg_autoctl/watch.c | 12 +- src/bin/pgaftest/compose_gen.c | 80 +- src/bin/pgaftest/test_runner.c | 4 +- src/bin/pgaftest/test_spec_parse.c | 1882 +++++++++++---------- src/bin/pgaftest/test_spec_scan.c | 617 ++++++- src/monitor/formation_metadata.c | 2 + src/monitor/health_check_worker.c | 6 +- src/monitor/node_metadata.c | 2 + src/monitor/node_metadata.h | 46 +- src/monitor/version_compat.h | 4 +- 33 files changed, 2495 insertions(+), 1824 deletions(-) diff --git a/src/bin/common/ini_file.h b/src/bin/common/ini_file.h index 7e3d61918..a19587a7c 100644 --- a/src/bin/common/ini_file.h +++ b/src/bin/common/ini_file.h @@ -59,38 +59,38 @@ typedef struct IniOption } IniOption; #define make_int_option(section, name, optName, required, value) \ - { INI_INT_T, section, name, optName, required, false, \ - NULL, -1, -1, NULL, NULL, value } + { INI_INT_T, section, name, optName, required, false, \ + NULL, -1, -1, NULL, NULL, value } #define make_int_option_default(section, name, optName, \ required, value, default) \ - { INI_INT_T, section, name, optName, required, false, \ - NULL, default, -1, NULL, NULL, value } + { INI_INT_T, section, name, optName, required, false, \ + NULL, default, -1, NULL, NULL, value } #define make_string_option(section, name, optName, required, value) \ - { INI_STRING_T, section, name, optName, required, false, \ - NULL, -1, -1, value, NULL, NULL } + { INI_STRING_T, section, name, optName, required, false, \ + NULL, -1, -1, value, NULL, NULL } #define make_string_option_default(section, name, optName, required, \ value, default) \ - { INI_STRING_T, section, name, optName, required, false, \ - default, -1, -1, value, NULL, NULL } + { INI_STRING_T, section, name, optName, required, false, \ + default, -1, -1, value, NULL, NULL } #define make_strbuf_option(section, name, optName, required, size, value) \ - { INI_STRBUF_T, section, name, optName, required, false, \ - NULL, -1, size, NULL, value, NULL } + { INI_STRBUF_T, section, name, optName, required, false, \ + NULL, -1, size, NULL, value, NULL } #define make_strbuf_compat_option(section, name, size, value) \ - { INI_STRBUF_T, section, name, NULL, false, true, \ - NULL, -1, size, NULL, value, NULL } + { INI_STRBUF_T, section, name, NULL, false, true, \ + NULL, -1, size, NULL, value, NULL } #define make_strbuf_option_default(section, name, optName, required, \ size, value, default) \ - { INI_STRBUF_T, section, name, optName, required, false, \ - default, -1, size, NULL, value, NULL } + { INI_STRBUF_T, section, name, optName, required, false, \ + default, -1, size, NULL, value, NULL } #define INI_OPTION_LAST \ - { INI_END_T, NULL, NULL, NULL, false, false, NULL, -1, -1, NULL, NULL, NULL } + { INI_END_T, NULL, NULL, NULL, false, false, NULL, -1, -1, NULL, NULL, NULL } bool read_ini_file(const char *filename, IniOption *opts); bool parse_ini_buffer(const char *filename, diff --git a/src/bin/common/pgctl.c b/src/bin/common/pgctl.c index 8ad756ae3..893a1b799 100644 --- a/src/bin/common/pgctl.c +++ b/src/bin/common/pgctl.c @@ -35,7 +35,7 @@ #include "runprogram.h" #define AUTOCTL_CONF_INCLUDE_COMMENT \ - " # Auto-generated by pg_auto_failover, do not remove\n" + " # Auto-generated by pg_auto_failover, do not remove\n" #define AUTOCTL_CONF_INCLUDE_LINE "include '" AUTOCTL_DEFAULTS_CONF_FILENAME "'" #define AUTOCTL_SB_CONF_INCLUDE_LINE "include '" AUTOCTL_STANDBY_CONF_FILENAME "'" diff --git a/src/bin/common/pgsetup.c b/src/bin/common/pgsetup.c index c9e0b0ecf..f1bed87f0 100644 --- a/src/bin/common/pgsetup.c +++ b/src/bin/common/pgsetup.c @@ -386,7 +386,7 @@ pg_setup_init(PostgresSetup *pgSetup, else { log_debug("Found PostgreSQL system %" PRIu64 " at \"%s\", " - "version %u, catalog version %u", + "version %u, catalog version %u", pgSetup->control.system_identifier, pgSetup->pgdata, pgSetup->control.pg_control_version, diff --git a/src/bin/common/pgsetup.h b/src/bin/common/pgsetup.h index f4d4b182d..4fbfb383f 100644 --- a/src/bin/common/pgsetup.h +++ b/src/bin/common/pgsetup.h @@ -135,11 +135,11 @@ typedef enum PgInstanceKind #define NODE_KIND_CITUS_ANY \ - (NODE_KIND_CITUS_COORDINATOR | NODE_KIND_CITUS_WORKER) + (NODE_KIND_CITUS_COORDINATOR | NODE_KIND_CITUS_WORKER) #define IS_CITUS_INSTANCE_KIND(x) \ - (x == NODE_KIND_CITUS_COORDINATOR \ + (x == NODE_KIND_CITUS_COORDINATOR \ || x == NODE_KIND_CITUS_WORKER) #define pgKind_matches(x, y) ((x & y) != 0) diff --git a/src/bin/common/pgsql.c b/src/bin/common/pgsql.c index 4fe32a084..1f2c1ff01 100644 --- a/src/bin/common/pgsql.c +++ b/src/bin/common/pgsql.c @@ -602,7 +602,7 @@ pgsql_open_connection(PGSQL *pgsql) * to help anyone. A good trade-off seems to be a warning every 30s. */ #define SHOULD_WARN_AGAIN(duration) \ - (INSTR_TIME_GET_MILLISEC(duration) > 30000) + (INSTR_TIME_GET_MILLISEC(duration) > 30000) /* * pgsql_retry_open_connection loops over a PQping call until the remote server diff --git a/src/bin/common/pgsql.h b/src/bin/common/pgsql.h index 978f6b172..b5e3f02c7 100644 --- a/src/bin/common/pgsql.h +++ b/src/bin/common/pgsql.h @@ -293,29 +293,29 @@ typedef struct SingleValueResultContext #define CHECK__SETTINGS_SQL \ - "select bool_and(ok) " \ - "from (" \ - "select current_setting('max_wal_senders')::int >= 12" \ - " union all " \ - "select current_setting('max_replication_slots')::int >= 12" \ - " union all " \ - "select current_setting('wal_level') in ('replica', 'logical')" \ - " union all " \ - "select current_setting('wal_log_hints') = 'on'" + "select bool_and(ok) " \ + "from (" \ + "select current_setting('max_wal_senders')::int >= 12" \ + " union all " \ + "select current_setting('max_replication_slots')::int >= 12" \ + " union all " \ + "select current_setting('wal_level') in ('replica', 'logical')" \ + " union all " \ + "select current_setting('wal_log_hints') = 'on'" #define CHECK_POSTGRESQL_NODE_SETTINGS_SQL \ - CHECK__SETTINGS_SQL \ - ") as t(ok) " + CHECK__SETTINGS_SQL \ + ") as t(ok) " #define CHECK_CITUS_NODE_SETTINGS_SQL \ - CHECK__SETTINGS_SQL \ - " union all " \ - "select lib = 'citus' " \ - "from unnest(string_to_array(" \ - "current_setting('shared_preload_libraries'), ',') " \ - " || array['not citus']) " \ - "with ordinality ast(lib, n) where n = 1" \ - ") as t(ok) " + CHECK__SETTINGS_SQL \ + " union all " \ + "select lib = 'citus' " \ + "from unnest(string_to_array(" \ + "current_setting('shared_preload_libraries'), ',') " \ + " || array['not citus']) " \ + "with ordinality ast(lib, n) where n = 1" \ + ") as t(ok) " bool pgsql_init(PGSQL *pgsql, char *url, ConnectionType connectionType); diff --git a/src/bin/pg_autoctl/cli_common.h b/src/bin/pg_autoctl/cli_common.h index 66c62fa40..68d5ddbad 100644 --- a/src/bin/pg_autoctl/cli_common.h +++ b/src/bin/pg_autoctl/cli_common.h @@ -36,47 +36,47 @@ extern int ssl_flag; extern int monitorDisabledNodeId; #define KEEPER_CLI_SSL_OPTIONS \ - " --ssl-self-signed setup network encryption using self signed certificates (does NOT protect against MITM)\n" \ - " --ssl-mode use that sslmode in connection strings\n" \ - " --ssl-ca-file set the Postgres ssl_ca_file to that file path\n" \ - " --ssl-crl-file set the Postgres ssl_crl_file to that file path\n" \ - " --no-ssl don't enable network encryption (NOT recommended, prefer --ssl-self-signed)\n" \ - " --server-key set the Postgres ssl_key_file to that file path\n" \ - " --server-cert set the Postgres ssl_cert_file to that file path\n" + " --ssl-self-signed setup network encryption using self signed certificates (does NOT protect against MITM)\n" \ + " --ssl-mode use that sslmode in connection strings\n" \ + " --ssl-ca-file set the Postgres ssl_ca_file to that file path\n" \ + " --ssl-crl-file set the Postgres ssl_crl_file to that file path\n" \ + " --no-ssl don't enable network encryption (NOT recommended, prefer --ssl-self-signed)\n" \ + " --server-key set the Postgres ssl_key_file to that file path\n" \ + " --server-cert set the Postgres ssl_cert_file to that file path\n" #define KEEPER_CLI_WORKER_SETUP_OPTIONS \ - " --pgctl path to pg_ctl\n" \ - " --pgdata path to data directory\n" \ - " --pghost PostgreSQL's hostname\n" \ - " --pgport PostgreSQL's port number\n" \ - " --listen PostgreSQL's listen_addresses\n" \ - " --username PostgreSQL's username\n" \ - " --dbname PostgreSQL's database name\n" \ - " --proxyport Proxy's port number\n" \ - " --name pg_auto_failover node name\n" \ - " --hostname hostname used to connect from the other nodes\n" \ - " --formation pg_auto_failover formation\n" \ - " --group pg_auto_failover group Id\n" \ - " --monitor pg_auto_failover Monitor Postgres URL\n" \ - KEEPER_CLI_SSL_OPTIONS + " --pgctl path to pg_ctl\n" \ + " --pgdata path to data directory\n" \ + " --pghost PostgreSQL's hostname\n" \ + " --pgport PostgreSQL's port number\n" \ + " --listen PostgreSQL's listen_addresses\n" \ + " --username PostgreSQL's username\n" \ + " --dbname PostgreSQL's database name\n" \ + " --proxyport Proxy's port number\n" \ + " --name pg_auto_failover node name\n" \ + " --hostname hostname used to connect from the other nodes\n" \ + " --formation pg_auto_failover formation\n" \ + " --group pg_auto_failover group Id\n" \ + " --monitor pg_auto_failover Monitor Postgres URL\n" \ + KEEPER_CLI_SSL_OPTIONS #define KEEPER_CLI_NON_WORKER_SETUP_OPTIONS \ - " --pgctl path to pg_ctl\n" \ - " --pgdata path to data directory\n" \ - " --pghost PostgreSQL's hostname\n" \ - " --pgport PostgreSQL's port number\n" \ - " --listen PostgreSQL's listen_addresses\n" \ - " --username PostgreSQL's username\n" \ - " --dbname PostgreSQL's database name\n" \ - " --name pg_auto_failover node name\n" \ - " --hostname hostname used to connect from the other nodes\n" \ - " --formation pg_auto_failover formation\n" \ - " --group pg_auto_failover group Id\n" \ - " --monitor pg_auto_failover Monitor Postgres URL\n" \ - KEEPER_CLI_SSL_OPTIONS + " --pgctl path to pg_ctl\n" \ + " --pgdata path to data directory\n" \ + " --pghost PostgreSQL's hostname\n" \ + " --pgport PostgreSQL's port number\n" \ + " --listen PostgreSQL's listen_addresses\n" \ + " --username PostgreSQL's username\n" \ + " --dbname PostgreSQL's database name\n" \ + " --name pg_auto_failover node name\n" \ + " --hostname hostname used to connect from the other nodes\n" \ + " --formation pg_auto_failover formation\n" \ + " --group pg_auto_failover group Id\n" \ + " --monitor pg_auto_failover Monitor Postgres URL\n" \ + KEEPER_CLI_SSL_OPTIONS #define CLI_PGDATA_OPTION \ - " --pgdata path to data directory\n" \ + " --pgdata path to data directory\n" \ #define CLI_PGDATA_USAGE " [ --pgdata ] [ --json ] " diff --git a/src/bin/pg_autoctl/cli_do_misc.c b/src/bin/pg_autoctl/cli_do_misc.c index 73e3cafb7..377e7f75f 100644 --- a/src/bin/pg_autoctl/cli_do_misc.c +++ b/src/bin/pg_autoctl/cli_do_misc.c @@ -363,8 +363,9 @@ keeper_cli_pgsetup_wait_getopts(int argc, char **argv) if (!stringToInt(optarg, &pgsetup_wait_timeout) || pgsetup_wait_timeout <= 0) { - log_fatal("--timeout argument is not a valid positive integer: \"%s\"", - optarg); + log_fatal( + "--timeout argument is not a valid positive integer: \"%s\"", + optarg); exit(EXIT_CODE_BAD_ARGS); } log_trace("--timeout %d", pgsetup_wait_timeout); diff --git a/src/bin/pg_autoctl/cli_do_root.h b/src/bin/pg_autoctl/cli_do_root.h index 732c486da..888f68f9f 100644 --- a/src/bin/pg_autoctl/cli_do_root.h +++ b/src/bin/pg_autoctl/cli_do_root.h @@ -101,7 +101,7 @@ void keeper_cli_pgsetup_pg_ctl(int argc, char **argv); void keeper_cli_pgsetup_hba_lan(int argc, char **argv); void keeper_cli_pgsetup_discover(int argc, char **argv); void keeper_cli_pgsetup_is_ready(int argc, char **argv); -int keeper_cli_pgsetup_wait_getopts(int argc, char **argv); +int keeper_cli_pgsetup_wait_getopts(int argc, char **argv); void keeper_cli_pgsetup_wait_until_ready(int argc, char **argv); void keeper_cli_pgsetup_startup_logs(int argc, char **argv); void keeper_cli_pgsetup_tune(int argc, char **argv); diff --git a/src/bin/pg_autoctl/config.c b/src/bin/pg_autoctl/config.c index 4a9f600f1..898867a53 100644 --- a/src/bin/pg_autoctl/config.c +++ b/src/bin/pg_autoctl/config.c @@ -70,10 +70,11 @@ build_xdg_path(char *dst, } default: - + { /* developper error */ log_error("No support for XDG Resource Type %d", xdgType); return false; + } } if (!get_env_copy_with_fallback(envVarName, xdg_topdir, MAXPGPATH, fallback)) diff --git a/src/bin/pg_autoctl/config.h b/src/bin/pg_autoctl/config.h index 325474b2b..4d036c90f 100644 --- a/src/bin/pg_autoctl/config.h +++ b/src/bin/pg_autoctl/config.h @@ -77,7 +77,7 @@ pgAutoCtlNodeRole ProbeConfigurationFileRole(const char *filename); #define strneq(x, y) \ - ((x != NULL) && (y != NULL) && (strcmp(x, y) != 0)) + ((x != NULL) && (y != NULL) && (strcmp(x, y) != 0)) bool config_accept_new_ssloptions(PostgresSetup *pgSetup, PostgresSetup *newPgSetup); diff --git a/src/bin/pg_autoctl/demoapp.c b/src/bin/pg_autoctl/demoapp.c index 609c75c97..c12bd3b11 100644 --- a/src/bin/pg_autoctl/demoapp.c +++ b/src/bin/pg_autoctl/demoapp.c @@ -546,10 +546,10 @@ demoapp_update_client_failovers(const char *pguri, int clientId, int failovers) */ #if PG_MAJORVERSION_NUM < 15 #define random_between(M, N) \ - ((M) + pg_lrand48() / (RAND_MAX / ((N) -(M) +1) + 1)) + ((M) + pg_lrand48() / (RAND_MAX / ((N) -(M) +1) + 1)) #else #define random_between(M, N) \ - ((M) + pg_prng_uint32(&prng_state) / (RAND_MAX / ((N) -(M) +1) + 1)) + ((M) + pg_prng_uint32(&prng_state) / (RAND_MAX / ((N) -(M) +1) + 1)) #endif /* diff --git a/src/bin/pg_autoctl/fsm.c b/src/bin/pg_autoctl/fsm.c index b5dcc523a..38ad2714d 100644 --- a/src/bin/pg_autoctl/fsm.c +++ b/src/bin/pg_autoctl/fsm.c @@ -30,156 +30,156 @@ * Comments displayed in the logs when state changes. */ #define COMMENT_INIT_TO_SINGLE \ - "Start as a single node" + "Start as a single node" #define COMMENT_PRIMARY_TO_SINGLE \ - "Other node was forcibly removed, now single" + "Other node was forcibly removed, now single" #define COMMENT_DEMOTED_TO_SINGLE \ - "Was demoted after a failure, " \ - "but secondary was forcibly removed" + "Was demoted after a failure, " \ + "but secondary was forcibly removed" #define COMMENT_LOST_PRIMARY \ - "Primary was forcibly removed" + "Primary was forcibly removed" #define COMMENT_REPLICATION_TO_SINGLE \ - "Went down to force the primary to time out, " \ - "but then it was removed" + "Went down to force the primary to time out, " \ + "but then it was removed" #define COMMENT_SINGLE_TO_WAIT_PRIMARY \ - "A new secondary was added" + "A new secondary was added" #define COMMENT_PRIMARY_TO_WAIT_PRIMARY \ - "Secondary became unhealthy" + "Secondary became unhealthy" #define COMMENT_PRIMARY_TO_JOIN_PRIMARY \ - "A new secondary was added" + "A new secondary was added" #define COMMENT_PRIMARY_TO_DRAINING \ - "A failover occurred, stopping writes " + "A failover occurred, stopping writes " #define COMMENT_PRIMARY_TO_PREPARE_MAINTENANCE \ - "Promoting the standby to enable maintenance on the " \ - "primary, stopping Postgres " + "Promoting the standby to enable maintenance on the " \ + "primary, stopping Postgres " #define COMMENT_PRIMARY_TO_MAINTENANCE \ - "Setting up Postgres in standby mode for maintenance operations" + "Setting up Postgres in standby mode for maintenance operations" #define COMMENT_PRIMARY_TO_MAINTENANCE_PROMOTE_SECONDARY \ - "Promoting the standby to enable maintenance on the primary" + "Promoting the standby to enable maintenance on the primary" #define COMMENT_PRIMARY_TO_DEMOTED \ - "A failover occurred, no longer primary" + "A failover occurred, no longer primary" #define COMMENT_DRAINING_TO_DEMOTED \ - "Demoted after a failover, no longer primary" + "Demoted after a failover, no longer primary" #define COMMENT_DRAINING_TO_DEMOTE_TIMEOUT \ - "Secondary confirms it’s receiving no more writes" + "Secondary confirms it’s receiving no more writes" #define COMMENT_DEMOTE_TIMEOUT_TO_DEMOTED \ - "Demote timeout expired" + "Demote timeout expired" #define COMMENT_STOP_REPLICATION_TO_WAIT_PRIMARY \ - "Confirmed promotion with the monitor" + "Confirmed promotion with the monitor" #define COMMENT_WAIT_PRIMARY_TO_PRIMARY \ - "A healthy secondary appeared" + "A healthy secondary appeared" #define COMMENT_JOIN_PRIMARY_TO_PRIMARY \ - "A healthy secondary appeared" + "A healthy secondary appeared" #define COMMENT_DEMOTE_TO_PRIMARY \ - "Detected a network partition, " \ - "but monitor didn't do failover" + "Detected a network partition, " \ + "but monitor didn't do failover" #define COMMENT_WAIT_STANDBY_TO_CATCHINGUP \ - "The primary is now ready to accept a standby" + "The primary is now ready to accept a standby" #define COMMENT_DEMOTED_TO_CATCHINGUP \ - "A new primary is available. " \ - "First, try to rewind. If that fails, do a pg_basebackup." + "A new primary is available. " \ + "First, try to rewind. If that fails, do a pg_basebackup." #define COMMENT_SECONDARY_TO_CATCHINGUP \ - "Failed to report back to the monitor, " \ - "not eligible for promotion" + "Failed to report back to the monitor, " \ + "not eligible for promotion" #define COMMENT_CATCHINGUP_TO_SECONDARY \ - "Convinced the monitor that I'm up and running, " \ - "and eligible for promotion again" + "Convinced the monitor that I'm up and running, " \ + "and eligible for promotion again" #define COMMENT_SECONDARY_TO_PREP_PROMOTION \ - "Stop traffic to primary, " \ - "wait for it to finish draining." + "Stop traffic to primary, " \ + "wait for it to finish draining." #define COMMENT_PROMOTION_TO_STOP_REPLICATION \ - "Prevent against split-brain situations." + "Prevent against split-brain situations." #define COMMENT_INIT_TO_WAIT_STANDBY \ - "Start following a primary" + "Start following a primary" #define COMMENT_SECONARY_TO_WAIT_STANDBY \ - "Registering to a new monitor" + "Registering to a new monitor" #define COMMENT_SECONDARY_TO_WAIT_MAINTENANCE \ - "Waiting for the primary to disable sync replication before " \ - "going to maintenance." + "Waiting for the primary to disable sync replication before " \ + "going to maintenance." #define COMMENT_SECONDARY_TO_MAINTENANCE \ - "Suspending standby for manual maintenance." + "Suspending standby for manual maintenance." #define COMMENT_MAINTENANCE_TO_CATCHINGUP \ - "Restarting standby after manual maintenance is done." + "Restarting standby after manual maintenance is done." #define COMMENT_BLOCKED_WRITES \ - "Promoting a Citus Worker standby after having blocked writes " \ - "from the coordinator." + "Promoting a Citus Worker standby after having blocked writes " \ + "from the coordinator." #define COMMENT_PRIMARY_TO_APPLY_SETTINGS \ - "Apply new pg_auto_failover settings (synchronous_standby_names)" + "Apply new pg_auto_failover settings (synchronous_standby_names)" #define COMMENT_APPLY_SETTINGS_TO_PRIMARY \ - "Back to primary state after having applied new pg_auto_failover settings" + "Back to primary state after having applied new pg_auto_failover settings" #define COMMENT_SECONDARY_TO_REPORT_LSN \ - "Reporting the last write-ahead log location received" + "Reporting the last write-ahead log location received" #define COMMENT_DRAINING_TO_REPORT_LSN \ - "Reporting the last write-ahead log location after draining" + "Reporting the last write-ahead log location after draining" #define COMMENT_DEMOTED_TO_REPORT_LSN \ - "Reporting the last write-ahead log location after being demoted" + "Reporting the last write-ahead log location after being demoted" #define COMMENT_REPORT_LSN_TO_PREP_PROMOTION \ - "Stop traffic to primary, " \ - "wait for it to finish draining." + "Stop traffic to primary, " \ + "wait for it to finish draining." #define COMMENT_REPORT_LSN_TO_FAST_FORWARD \ - "Fetching missing WAL bits from another standby before promotion" + "Fetching missing WAL bits from another standby before promotion" #define COMMENT_REPORT_LSN_TO_SINGLE \ - "There is no other node anymore, promote this node" + "There is no other node anymore, promote this node" #define COMMENT_FOLLOW_NEW_PRIMARY \ - "Switch replication to the new primary" + "Switch replication to the new primary" #define COMMENT_REPORT_LSN_TO_JOIN_SECONDARY \ - "A failover candidate has been selected, stop replication" + "A failover candidate has been selected, stop replication" #define COMMENT_JOIN_SECONDARY_TO_SECONDARY \ - "Failover is done, we have a new primary to follow" + "Failover is done, we have a new primary to follow" #define COMMENT_FAST_FORWARD_TO_PREP_PROMOTION \ - "Got the missing WAL bytes, promoted" + "Got the missing WAL bytes, promoted" #define COMMENT_INIT_TO_REPORT_LSN \ - "Creating a new node from a standby node that is not a candidate." + "Creating a new node from a standby node that is not a candidate." #define COMMENT_DROPPED_TO_REPORT_LSN \ - "This node is being reinitialized after having been dropped" + "This node is being reinitialized after having been dropped" #define COMMENT_ANY_TO_DROPPED \ - "This node is being dropped from the monitor" + "This node is being dropped from the monitor" /* @@ -201,372 +201,526 @@ KeeperFSMTransition KeeperFSM[] = { /* * Started as a single, no nothing */ - { INIT_STATE, SINGLE_STATE, NODE_KIND_CITUS_COORDINATOR, - COMMENT_INIT_TO_SINGLE, - &fsm_citus_coordinator_init_primary }, + { + INIT_STATE, SINGLE_STATE, NODE_KIND_CITUS_COORDINATOR, + COMMENT_INIT_TO_SINGLE, + &fsm_citus_coordinator_init_primary + }, - { INIT_STATE, SINGLE_STATE, NODE_KIND_CITUS_WORKER, - COMMENT_INIT_TO_SINGLE, - &fsm_citus_worker_init_primary }, + { + INIT_STATE, SINGLE_STATE, NODE_KIND_CITUS_WORKER, + COMMENT_INIT_TO_SINGLE, + &fsm_citus_worker_init_primary + }, - { INIT_STATE, SINGLE_STATE, NODE_KIND_ANY, - COMMENT_INIT_TO_SINGLE, - &fsm_init_primary }, + { + INIT_STATE, SINGLE_STATE, NODE_KIND_ANY, + COMMENT_INIT_TO_SINGLE, + &fsm_init_primary + }, - { DROPPED_STATE, SINGLE_STATE, NODE_KIND_CITUS_WORKER, - COMMENT_INIT_TO_SINGLE, - &fsm_citus_worker_init_primary }, + { + DROPPED_STATE, SINGLE_STATE, NODE_KIND_CITUS_WORKER, + COMMENT_INIT_TO_SINGLE, + &fsm_citus_worker_init_primary + }, - { DROPPED_STATE, SINGLE_STATE, NODE_KIND_ANY, - COMMENT_INIT_TO_SINGLE, - &fsm_init_primary }, + { + DROPPED_STATE, SINGLE_STATE, NODE_KIND_ANY, + COMMENT_INIT_TO_SINGLE, + &fsm_init_primary + }, - { DROPPED_STATE, REPORT_LSN_STATE, NODE_KIND_ANY, - COMMENT_DROPPED_TO_REPORT_LSN, - &fsm_init_from_standby }, + { + DROPPED_STATE, REPORT_LSN_STATE, NODE_KIND_ANY, + COMMENT_DROPPED_TO_REPORT_LSN, + &fsm_init_from_standby + }, /* * other node(s) was forcibly removed, now single */ - { PRIMARY_STATE, SINGLE_STATE, NODE_KIND_ANY, - COMMENT_PRIMARY_TO_SINGLE, - &fsm_disable_replication }, + { + PRIMARY_STATE, SINGLE_STATE, NODE_KIND_ANY, + COMMENT_PRIMARY_TO_SINGLE, + &fsm_disable_replication + }, - { WAIT_PRIMARY_STATE, SINGLE_STATE, NODE_KIND_ANY, - COMMENT_PRIMARY_TO_SINGLE, - &fsm_disable_replication }, + { + WAIT_PRIMARY_STATE, SINGLE_STATE, NODE_KIND_ANY, + COMMENT_PRIMARY_TO_SINGLE, + &fsm_disable_replication + }, - { JOIN_PRIMARY_STATE, SINGLE_STATE, NODE_KIND_ANY, - COMMENT_PRIMARY_TO_SINGLE, - &fsm_disable_replication }, + { + JOIN_PRIMARY_STATE, SINGLE_STATE, NODE_KIND_ANY, + COMMENT_PRIMARY_TO_SINGLE, + &fsm_disable_replication + }, /* * failover occurred, primary -> draining/demoted */ - { PRIMARY_STATE, DRAINING_STATE, NODE_KIND_ANY, - COMMENT_PRIMARY_TO_DRAINING, - &fsm_stop_postgres }, + { + PRIMARY_STATE, DRAINING_STATE, NODE_KIND_ANY, + COMMENT_PRIMARY_TO_DRAINING, + &fsm_stop_postgres + }, - { DRAINING_STATE, DEMOTED_STATE, NODE_KIND_ANY, - COMMENT_DRAINING_TO_DEMOTED, - &fsm_stop_postgres }, + { + DRAINING_STATE, DEMOTED_STATE, NODE_KIND_ANY, + COMMENT_DRAINING_TO_DEMOTED, + &fsm_stop_postgres + }, - { PRIMARY_STATE, DEMOTED_STATE, NODE_KIND_ANY, - COMMENT_PRIMARY_TO_DEMOTED, - &fsm_stop_postgres }, + { + PRIMARY_STATE, DEMOTED_STATE, NODE_KIND_ANY, + COMMENT_PRIMARY_TO_DEMOTED, + &fsm_stop_postgres + }, - { PRIMARY_STATE, DEMOTE_TIMEOUT_STATE, NODE_KIND_ANY, - COMMENT_PRIMARY_TO_DEMOTED, - &fsm_stop_postgres }, + { + PRIMARY_STATE, DEMOTE_TIMEOUT_STATE, NODE_KIND_ANY, + COMMENT_PRIMARY_TO_DEMOTED, + &fsm_stop_postgres + }, - { JOIN_PRIMARY_STATE, DRAINING_STATE, NODE_KIND_ANY, - COMMENT_PRIMARY_TO_DRAINING, - &fsm_stop_postgres }, + { + JOIN_PRIMARY_STATE, DRAINING_STATE, NODE_KIND_ANY, + COMMENT_PRIMARY_TO_DRAINING, + &fsm_stop_postgres + }, - { JOIN_PRIMARY_STATE, DEMOTED_STATE, NODE_KIND_ANY, - COMMENT_PRIMARY_TO_DEMOTED, - &fsm_stop_postgres }, + { + JOIN_PRIMARY_STATE, DEMOTED_STATE, NODE_KIND_ANY, + COMMENT_PRIMARY_TO_DEMOTED, + &fsm_stop_postgres + }, - { JOIN_PRIMARY_STATE, DEMOTE_TIMEOUT_STATE, NODE_KIND_ANY, - COMMENT_PRIMARY_TO_DEMOTED, - &fsm_stop_postgres }, + { + JOIN_PRIMARY_STATE, DEMOTE_TIMEOUT_STATE, NODE_KIND_ANY, + COMMENT_PRIMARY_TO_DEMOTED, + &fsm_stop_postgres + }, - { APPLY_SETTINGS_STATE, DRAINING_STATE, NODE_KIND_ANY, - COMMENT_PRIMARY_TO_DRAINING, - &fsm_stop_postgres }, + { + APPLY_SETTINGS_STATE, DRAINING_STATE, NODE_KIND_ANY, + COMMENT_PRIMARY_TO_DRAINING, + &fsm_stop_postgres + }, - { APPLY_SETTINGS_STATE, DEMOTED_STATE, NODE_KIND_ANY, - COMMENT_PRIMARY_TO_DEMOTED, - &fsm_stop_postgres }, + { + APPLY_SETTINGS_STATE, DEMOTED_STATE, NODE_KIND_ANY, + COMMENT_PRIMARY_TO_DEMOTED, + &fsm_stop_postgres + }, - { APPLY_SETTINGS_STATE, DEMOTE_TIMEOUT_STATE, NODE_KIND_ANY, - COMMENT_PRIMARY_TO_DEMOTED, - &fsm_stop_postgres }, + { + APPLY_SETTINGS_STATE, DEMOTE_TIMEOUT_STATE, NODE_KIND_ANY, + COMMENT_PRIMARY_TO_DEMOTED, + &fsm_stop_postgres + }, /* * primary is put to maintenance */ - { PRIMARY_STATE, PREPARE_MAINTENANCE_STATE, NODE_KIND_ANY, - COMMENT_PRIMARY_TO_PREPARE_MAINTENANCE, - &fsm_stop_postgres_for_primary_maintenance }, + { + PRIMARY_STATE, PREPARE_MAINTENANCE_STATE, NODE_KIND_ANY, + COMMENT_PRIMARY_TO_PREPARE_MAINTENANCE, + &fsm_stop_postgres_for_primary_maintenance + }, - { PREPARE_MAINTENANCE_STATE, MAINTENANCE_STATE, NODE_KIND_ANY, - COMMENT_PRIMARY_TO_MAINTENANCE, - &fsm_stop_postgres_and_setup_standby }, + { + PREPARE_MAINTENANCE_STATE, MAINTENANCE_STATE, NODE_KIND_ANY, + COMMENT_PRIMARY_TO_MAINTENANCE, + &fsm_stop_postgres_and_setup_standby + }, - { PRIMARY_STATE, MAINTENANCE_STATE, NODE_KIND_ANY, - COMMENT_PRIMARY_TO_MAINTENANCE, - &fsm_stop_postgres_for_primary_maintenance }, + { + PRIMARY_STATE, MAINTENANCE_STATE, NODE_KIND_ANY, + COMMENT_PRIMARY_TO_MAINTENANCE, + &fsm_stop_postgres_for_primary_maintenance + }, /* * was demoted, need to be dead now. */ - { DRAINING_STATE, DEMOTE_TIMEOUT_STATE, NODE_KIND_ANY, - COMMENT_DRAINING_TO_DEMOTE_TIMEOUT, - &fsm_stop_postgres }, + { + DRAINING_STATE, DEMOTE_TIMEOUT_STATE, NODE_KIND_ANY, + COMMENT_DRAINING_TO_DEMOTE_TIMEOUT, + &fsm_stop_postgres + }, - { DEMOTE_TIMEOUT_STATE, DEMOTED_STATE, NODE_KIND_ANY, - COMMENT_DEMOTE_TIMEOUT_TO_DEMOTED, - &fsm_stop_postgres }, + { + DEMOTE_TIMEOUT_STATE, DEMOTED_STATE, NODE_KIND_ANY, + COMMENT_DEMOTE_TIMEOUT_TO_DEMOTED, + &fsm_stop_postgres + }, /* * wait_primary stops reporting, is (supposed) dead now */ - { WAIT_PRIMARY_STATE, DEMOTED_STATE, NODE_KIND_ANY, - COMMENT_PRIMARY_TO_DEMOTED, - &fsm_stop_postgres }, + { + WAIT_PRIMARY_STATE, DEMOTED_STATE, NODE_KIND_ANY, + COMMENT_PRIMARY_TO_DEMOTED, + &fsm_stop_postgres + }, /* * was demoted after a failure, but standby was forcibly removed */ - { DEMOTED_STATE, SINGLE_STATE, NODE_KIND_CITUS_WORKER, - COMMENT_DEMOTED_TO_SINGLE, - &fsm_citus_worker_resume_as_primary }, + { + DEMOTED_STATE, SINGLE_STATE, NODE_KIND_CITUS_WORKER, + COMMENT_DEMOTED_TO_SINGLE, + &fsm_citus_worker_resume_as_primary + }, - { DEMOTED_STATE, SINGLE_STATE, NODE_KIND_ANY, - COMMENT_DEMOTED_TO_SINGLE, - &fsm_resume_as_primary }, + { + DEMOTED_STATE, SINGLE_STATE, NODE_KIND_ANY, + COMMENT_DEMOTED_TO_SINGLE, + &fsm_resume_as_primary + }, - { DEMOTE_TIMEOUT_STATE, SINGLE_STATE, NODE_KIND_CITUS_WORKER, - COMMENT_DEMOTED_TO_SINGLE, - &fsm_citus_worker_resume_as_primary }, + { + DEMOTE_TIMEOUT_STATE, SINGLE_STATE, NODE_KIND_CITUS_WORKER, + COMMENT_DEMOTED_TO_SINGLE, + &fsm_citus_worker_resume_as_primary + }, - { DEMOTE_TIMEOUT_STATE, SINGLE_STATE, NODE_KIND_ANY, - COMMENT_DEMOTED_TO_SINGLE, - &fsm_resume_as_primary }, + { + DEMOTE_TIMEOUT_STATE, SINGLE_STATE, NODE_KIND_ANY, + COMMENT_DEMOTED_TO_SINGLE, + &fsm_resume_as_primary + }, - { DRAINING_STATE, SINGLE_STATE, NODE_KIND_CITUS_WORKER, - COMMENT_DEMOTED_TO_SINGLE, - &fsm_citus_worker_resume_as_primary }, + { + DRAINING_STATE, SINGLE_STATE, NODE_KIND_CITUS_WORKER, + COMMENT_DEMOTED_TO_SINGLE, + &fsm_citus_worker_resume_as_primary + }, - { DRAINING_STATE, SINGLE_STATE, NODE_KIND_ANY, - COMMENT_DEMOTED_TO_SINGLE, - &fsm_resume_as_primary }, + { + DRAINING_STATE, SINGLE_STATE, NODE_KIND_ANY, + COMMENT_DEMOTED_TO_SINGLE, + &fsm_resume_as_primary + }, /* * primary was forcibly removed */ - { SECONDARY_STATE, SINGLE_STATE, NODE_KIND_CITUS_COORDINATOR, - COMMENT_LOST_PRIMARY, - &fsm_citus_coordinator_promote_standby_to_single }, + { + SECONDARY_STATE, SINGLE_STATE, NODE_KIND_CITUS_COORDINATOR, + COMMENT_LOST_PRIMARY, + &fsm_citus_coordinator_promote_standby_to_single + }, - { SECONDARY_STATE, SINGLE_STATE, NODE_KIND_CITUS_WORKER, - COMMENT_LOST_PRIMARY, - &fsm_citus_worker_promote_standby_to_single }, + { + SECONDARY_STATE, SINGLE_STATE, NODE_KIND_CITUS_WORKER, + COMMENT_LOST_PRIMARY, + &fsm_citus_worker_promote_standby_to_single + }, - { SECONDARY_STATE, SINGLE_STATE, NODE_KIND_ANY, - COMMENT_LOST_PRIMARY, - &fsm_promote_standby }, + { + SECONDARY_STATE, SINGLE_STATE, NODE_KIND_ANY, + COMMENT_LOST_PRIMARY, + &fsm_promote_standby + }, - { CATCHINGUP_STATE, SINGLE_STATE, NODE_KIND_CITUS_COORDINATOR, - COMMENT_LOST_PRIMARY, - &fsm_citus_coordinator_promote_standby_to_single }, + { + CATCHINGUP_STATE, SINGLE_STATE, NODE_KIND_CITUS_COORDINATOR, + COMMENT_LOST_PRIMARY, + &fsm_citus_coordinator_promote_standby_to_single + }, - { CATCHINGUP_STATE, SINGLE_STATE, NODE_KIND_CITUS_WORKER, - COMMENT_LOST_PRIMARY, - &fsm_citus_worker_promote_standby_to_single }, + { + CATCHINGUP_STATE, SINGLE_STATE, NODE_KIND_CITUS_WORKER, + COMMENT_LOST_PRIMARY, + &fsm_citus_worker_promote_standby_to_single + }, - { CATCHINGUP_STATE, SINGLE_STATE, NODE_KIND_ANY, - COMMENT_LOST_PRIMARY, - &fsm_promote_standby }, + { + CATCHINGUP_STATE, SINGLE_STATE, NODE_KIND_ANY, + COMMENT_LOST_PRIMARY, + &fsm_promote_standby + }, - { PREP_PROMOTION_STATE, SINGLE_STATE, NODE_KIND_CITUS_COORDINATOR, - COMMENT_LOST_PRIMARY, - &fsm_citus_coordinator_promote_standby_to_single }, + { + PREP_PROMOTION_STATE, SINGLE_STATE, NODE_KIND_CITUS_COORDINATOR, + COMMENT_LOST_PRIMARY, + &fsm_citus_coordinator_promote_standby_to_single + }, - { PREP_PROMOTION_STATE, SINGLE_STATE, NODE_KIND_CITUS_WORKER, - COMMENT_LOST_PRIMARY, - &fsm_citus_worker_promote_standby_to_single }, + { + PREP_PROMOTION_STATE, SINGLE_STATE, NODE_KIND_CITUS_WORKER, + COMMENT_LOST_PRIMARY, + &fsm_citus_worker_promote_standby_to_single + }, - { PREP_PROMOTION_STATE, SINGLE_STATE, NODE_KIND_ANY, - COMMENT_LOST_PRIMARY, - &fsm_promote_standby }, + { + PREP_PROMOTION_STATE, SINGLE_STATE, NODE_KIND_ANY, + COMMENT_LOST_PRIMARY, + &fsm_promote_standby + }, /* * went down to force the primary to time out, but then it was removed */ - { STOP_REPLICATION_STATE, SINGLE_STATE, NODE_KIND_CITUS_WORKER, - COMMENT_REPLICATION_TO_SINGLE, - &fsm_citus_worker_promote_standby_to_single }, + { + STOP_REPLICATION_STATE, SINGLE_STATE, NODE_KIND_CITUS_WORKER, + COMMENT_REPLICATION_TO_SINGLE, + &fsm_citus_worker_promote_standby_to_single + }, - { STOP_REPLICATION_STATE, SINGLE_STATE, NODE_KIND_ANY, - COMMENT_REPLICATION_TO_SINGLE, - &fsm_promote_standby }, + { + STOP_REPLICATION_STATE, SINGLE_STATE, NODE_KIND_ANY, + COMMENT_REPLICATION_TO_SINGLE, + &fsm_promote_standby + }, /* * all states should lead to SINGLE, including REPORT_LSN */ - { REPORT_LSN_STATE, SINGLE_STATE, NODE_KIND_ANY, - COMMENT_REPORT_LSN_TO_SINGLE, - &fsm_promote_standby }, + { + REPORT_LSN_STATE, SINGLE_STATE, NODE_KIND_ANY, + COMMENT_REPORT_LSN_TO_SINGLE, + &fsm_promote_standby + }, /* * On the Primary, wait for a standby to be ready: WAIT_PRIMARY */ - { SINGLE_STATE, WAIT_PRIMARY_STATE, NODE_KIND_ANY, - COMMENT_SINGLE_TO_WAIT_PRIMARY, - &fsm_prepare_replication }, + { + SINGLE_STATE, WAIT_PRIMARY_STATE, NODE_KIND_ANY, + COMMENT_SINGLE_TO_WAIT_PRIMARY, + &fsm_prepare_replication + }, - { PRIMARY_STATE, JOIN_PRIMARY_STATE, NODE_KIND_ANY, - COMMENT_PRIMARY_TO_JOIN_PRIMARY, - &fsm_prepare_replication }, + { + PRIMARY_STATE, JOIN_PRIMARY_STATE, NODE_KIND_ANY, + COMMENT_PRIMARY_TO_JOIN_PRIMARY, + &fsm_prepare_replication + }, - { PRIMARY_STATE, WAIT_PRIMARY_STATE, NODE_KIND_ANY, - COMMENT_PRIMARY_TO_WAIT_PRIMARY, - &fsm_disable_sync_rep }, + { + PRIMARY_STATE, WAIT_PRIMARY_STATE, NODE_KIND_ANY, + COMMENT_PRIMARY_TO_WAIT_PRIMARY, + &fsm_disable_sync_rep + }, - { JOIN_PRIMARY_STATE, WAIT_PRIMARY_STATE, NODE_KIND_ANY, - COMMENT_PRIMARY_TO_WAIT_PRIMARY, - &fsm_disable_sync_rep }, + { + JOIN_PRIMARY_STATE, WAIT_PRIMARY_STATE, NODE_KIND_ANY, + COMMENT_PRIMARY_TO_WAIT_PRIMARY, + &fsm_disable_sync_rep + }, - { WAIT_PRIMARY_STATE, JOIN_PRIMARY_STATE, NODE_KIND_ANY, - COMMENT_PRIMARY_TO_JOIN_PRIMARY, - &fsm_prepare_replication }, + { + WAIT_PRIMARY_STATE, JOIN_PRIMARY_STATE, NODE_KIND_ANY, + COMMENT_PRIMARY_TO_JOIN_PRIMARY, + &fsm_prepare_replication + }, /* * Situation is getting back to normal on the primary */ - { WAIT_PRIMARY_STATE, PRIMARY_STATE, NODE_KIND_ANY, - COMMENT_WAIT_PRIMARY_TO_PRIMARY, - &fsm_enable_sync_rep }, + { + WAIT_PRIMARY_STATE, PRIMARY_STATE, NODE_KIND_ANY, + COMMENT_WAIT_PRIMARY_TO_PRIMARY, + &fsm_enable_sync_rep + }, - { JOIN_PRIMARY_STATE, PRIMARY_STATE, NODE_KIND_ANY, - COMMENT_JOIN_PRIMARY_TO_PRIMARY, - &fsm_enable_sync_rep }, + { + JOIN_PRIMARY_STATE, PRIMARY_STATE, NODE_KIND_ANY, + COMMENT_JOIN_PRIMARY_TO_PRIMARY, + &fsm_enable_sync_rep + }, - { DEMOTE_TIMEOUT_STATE, PRIMARY_STATE, NODE_KIND_ANY, - COMMENT_DEMOTE_TO_PRIMARY, - &fsm_start_postgres }, + { + DEMOTE_TIMEOUT_STATE, PRIMARY_STATE, NODE_KIND_ANY, + COMMENT_DEMOTE_TO_PRIMARY, + &fsm_start_postgres + }, /* * The primary is now ready to accept a standby, we're the standby */ - { WAIT_STANDBY_STATE, CATCHINGUP_STATE, NODE_KIND_ANY, - COMMENT_WAIT_STANDBY_TO_CATCHINGUP, - &fsm_init_standby }, + { + WAIT_STANDBY_STATE, CATCHINGUP_STATE, NODE_KIND_ANY, + COMMENT_WAIT_STANDBY_TO_CATCHINGUP, + &fsm_init_standby + }, - { DEMOTED_STATE, CATCHINGUP_STATE, NODE_KIND_ANY, - COMMENT_DEMOTED_TO_CATCHINGUP, - &fsm_rewind_or_init }, + { + DEMOTED_STATE, CATCHINGUP_STATE, NODE_KIND_ANY, + COMMENT_DEMOTED_TO_CATCHINGUP, + &fsm_rewind_or_init + }, - { SECONDARY_STATE, CATCHINGUP_STATE, NODE_KIND_ANY, - COMMENT_SECONDARY_TO_CATCHINGUP, - &fsm_follow_new_primary }, + { + SECONDARY_STATE, CATCHINGUP_STATE, NODE_KIND_ANY, + COMMENT_SECONDARY_TO_CATCHINGUP, + &fsm_follow_new_primary + }, /* * We're asked to be a standby. */ - { CATCHINGUP_STATE, SECONDARY_STATE, NODE_KIND_CITUS_ANY, - COMMENT_CATCHINGUP_TO_SECONDARY, - &fsm_citus_maintain_replication_slots }, + { + CATCHINGUP_STATE, SECONDARY_STATE, NODE_KIND_CITUS_ANY, + COMMENT_CATCHINGUP_TO_SECONDARY, + &fsm_citus_maintain_replication_slots + }, - { CATCHINGUP_STATE, SECONDARY_STATE, NODE_KIND_ANY, - COMMENT_CATCHINGUP_TO_SECONDARY, - &fsm_prepare_for_secondary }, + { + CATCHINGUP_STATE, SECONDARY_STATE, NODE_KIND_ANY, + COMMENT_CATCHINGUP_TO_SECONDARY, + &fsm_prepare_for_secondary + }, /* * The standby is asked to prepare its own promotion */ - { SECONDARY_STATE, PREP_PROMOTION_STATE, NODE_KIND_CITUS_WORKER, - COMMENT_SECONDARY_TO_PREP_PROMOTION, - &fsm_citus_worker_prepare_standby_for_promotion }, + { + SECONDARY_STATE, PREP_PROMOTION_STATE, NODE_KIND_CITUS_WORKER, + COMMENT_SECONDARY_TO_PREP_PROMOTION, + &fsm_citus_worker_prepare_standby_for_promotion + }, - { SECONDARY_STATE, PREP_PROMOTION_STATE, NODE_KIND_ANY, - COMMENT_SECONDARY_TO_PREP_PROMOTION, - &fsm_prepare_standby_for_promotion }, + { + SECONDARY_STATE, PREP_PROMOTION_STATE, NODE_KIND_ANY, + COMMENT_SECONDARY_TO_PREP_PROMOTION, + &fsm_prepare_standby_for_promotion + }, - { CATCHINGUP_STATE, PREP_PROMOTION_STATE, NODE_KIND_CITUS_WORKER, - COMMENT_SECONDARY_TO_PREP_PROMOTION, - &fsm_citus_worker_prepare_standby_for_promotion }, + { + CATCHINGUP_STATE, PREP_PROMOTION_STATE, NODE_KIND_CITUS_WORKER, + COMMENT_SECONDARY_TO_PREP_PROMOTION, + &fsm_citus_worker_prepare_standby_for_promotion + }, - { CATCHINGUP_STATE, PREP_PROMOTION_STATE, NODE_KIND_ANY, - COMMENT_SECONDARY_TO_PREP_PROMOTION, - &fsm_prepare_standby_for_promotion }, + { + CATCHINGUP_STATE, PREP_PROMOTION_STATE, NODE_KIND_ANY, + COMMENT_SECONDARY_TO_PREP_PROMOTION, + &fsm_prepare_standby_for_promotion + }, /* * Forcefully stop replication by stopping the server. */ - { PREP_PROMOTION_STATE, STOP_REPLICATION_STATE, NODE_KIND_CITUS_WORKER, - COMMENT_PROMOTION_TO_STOP_REPLICATION, - &fsm_citus_worker_stop_replication }, + { + PREP_PROMOTION_STATE, STOP_REPLICATION_STATE, NODE_KIND_CITUS_WORKER, + COMMENT_PROMOTION_TO_STOP_REPLICATION, + &fsm_citus_worker_stop_replication + }, - { PREP_PROMOTION_STATE, STOP_REPLICATION_STATE, NODE_KIND_ANY, - COMMENT_PROMOTION_TO_STOP_REPLICATION, - &fsm_stop_replication }, + { + PREP_PROMOTION_STATE, STOP_REPLICATION_STATE, NODE_KIND_ANY, + COMMENT_PROMOTION_TO_STOP_REPLICATION, + &fsm_stop_replication + }, /* * finish the promotion */ - { STOP_REPLICATION_STATE, WAIT_PRIMARY_STATE, NODE_KIND_CITUS_COORDINATOR, - COMMENT_STOP_REPLICATION_TO_WAIT_PRIMARY, - &fsm_citus_coordinator_promote_standby_to_primary }, + { + STOP_REPLICATION_STATE, WAIT_PRIMARY_STATE, NODE_KIND_CITUS_COORDINATOR, + COMMENT_STOP_REPLICATION_TO_WAIT_PRIMARY, + &fsm_citus_coordinator_promote_standby_to_primary + }, - { STOP_REPLICATION_STATE, WAIT_PRIMARY_STATE, NODE_KIND_CITUS_WORKER, - COMMENT_STOP_REPLICATION_TO_WAIT_PRIMARY, - &fsm_citus_worker_promote_standby_to_primary }, + { + STOP_REPLICATION_STATE, WAIT_PRIMARY_STATE, NODE_KIND_CITUS_WORKER, + COMMENT_STOP_REPLICATION_TO_WAIT_PRIMARY, + &fsm_citus_worker_promote_standby_to_primary + }, - { STOP_REPLICATION_STATE, WAIT_PRIMARY_STATE, NODE_KIND_ANY, - COMMENT_STOP_REPLICATION_TO_WAIT_PRIMARY, - &fsm_promote_standby_to_primary }, + { + STOP_REPLICATION_STATE, WAIT_PRIMARY_STATE, NODE_KIND_ANY, + COMMENT_STOP_REPLICATION_TO_WAIT_PRIMARY, + &fsm_promote_standby_to_primary + }, - { PREP_PROMOTION_STATE, WAIT_PRIMARY_STATE, NODE_KIND_CITUS_WORKER, - COMMENT_BLOCKED_WRITES, - &fsm_citus_worker_promote_standby }, + { + PREP_PROMOTION_STATE, WAIT_PRIMARY_STATE, NODE_KIND_CITUS_WORKER, + COMMENT_BLOCKED_WRITES, + &fsm_citus_worker_promote_standby + }, - { PREP_PROMOTION_STATE, WAIT_PRIMARY_STATE, NODE_KIND_ANY, - COMMENT_BLOCKED_WRITES, - &fsm_promote_standby }, + { + PREP_PROMOTION_STATE, WAIT_PRIMARY_STATE, NODE_KIND_ANY, + COMMENT_BLOCKED_WRITES, + &fsm_promote_standby + }, /* * Just wait until primary is ready */ - { INIT_STATE, WAIT_STANDBY_STATE, NODE_KIND_ANY, - COMMENT_INIT_TO_WAIT_STANDBY, - NULL }, + { + INIT_STATE, WAIT_STANDBY_STATE, NODE_KIND_ANY, + COMMENT_INIT_TO_WAIT_STANDBY, + NULL + }, - { DROPPED_STATE, WAIT_STANDBY_STATE, NODE_KIND_ANY, - COMMENT_INIT_TO_WAIT_STANDBY, - NULL }, + { + DROPPED_STATE, WAIT_STANDBY_STATE, NODE_KIND_ANY, + COMMENT_INIT_TO_WAIT_STANDBY, + NULL + }, /* * When losing a monitor and then connecting to a new monitor as a * secondary, we need to be able to follow the init sequence again. */ - { SECONDARY_STATE, WAIT_STANDBY_STATE, NODE_KIND_ANY, - COMMENT_SECONARY_TO_WAIT_STANDBY, - NULL }, + { + SECONDARY_STATE, WAIT_STANDBY_STATE, NODE_KIND_ANY, + COMMENT_SECONARY_TO_WAIT_STANDBY, + NULL + }, /* * In case of maintenance of the standby server, we stop PostgreSQL. */ - { SECONDARY_STATE, WAIT_MAINTENANCE_STATE, NODE_KIND_ANY, - COMMENT_SECONDARY_TO_WAIT_MAINTENANCE, - NULL }, + { + SECONDARY_STATE, WAIT_MAINTENANCE_STATE, NODE_KIND_ANY, + COMMENT_SECONDARY_TO_WAIT_MAINTENANCE, + NULL + }, - { CATCHINGUP_STATE, WAIT_MAINTENANCE_STATE, NODE_KIND_ANY, - COMMENT_SECONDARY_TO_WAIT_MAINTENANCE, - NULL }, + { + CATCHINGUP_STATE, WAIT_MAINTENANCE_STATE, NODE_KIND_ANY, + COMMENT_SECONDARY_TO_WAIT_MAINTENANCE, + NULL + }, - { SECONDARY_STATE, MAINTENANCE_STATE, NODE_KIND_ANY, - COMMENT_SECONDARY_TO_MAINTENANCE, - &fsm_start_maintenance_on_standby }, + { + SECONDARY_STATE, MAINTENANCE_STATE, NODE_KIND_ANY, + COMMENT_SECONDARY_TO_MAINTENANCE, + &fsm_start_maintenance_on_standby + }, - { CATCHINGUP_STATE, MAINTENANCE_STATE, NODE_KIND_ANY, - COMMENT_SECONDARY_TO_MAINTENANCE, - &fsm_start_maintenance_on_standby }, + { + CATCHINGUP_STATE, MAINTENANCE_STATE, NODE_KIND_ANY, + COMMENT_SECONDARY_TO_MAINTENANCE, + &fsm_start_maintenance_on_standby + }, - { WAIT_MAINTENANCE_STATE, MAINTENANCE_STATE, NODE_KIND_ANY, - COMMENT_SECONDARY_TO_MAINTENANCE, - &fsm_start_maintenance_on_standby }, + { + WAIT_MAINTENANCE_STATE, MAINTENANCE_STATE, NODE_KIND_ANY, + COMMENT_SECONDARY_TO_MAINTENANCE, + &fsm_start_maintenance_on_standby + }, - { MAINTENANCE_STATE, CATCHINGUP_STATE, NODE_KIND_ANY, - COMMENT_MAINTENANCE_TO_CATCHINGUP, - &fsm_restart_standby }, + { + MAINTENANCE_STATE, CATCHINGUP_STATE, NODE_KIND_ANY, + COMMENT_MAINTENANCE_TO_CATCHINGUP, + &fsm_restart_standby + }, - { PREPARE_MAINTENANCE_STATE, CATCHINGUP_STATE, NODE_KIND_ANY, - COMMENT_MAINTENANCE_TO_CATCHINGUP, - &fsm_restart_standby }, + { + PREPARE_MAINTENANCE_STATE, CATCHINGUP_STATE, NODE_KIND_ANY, + COMMENT_MAINTENANCE_TO_CATCHINGUP, + &fsm_restart_standby + }, /* * Applying new replication/cluster settings (per node replication quorum, @@ -574,119 +728,167 @@ KeeperFSMTransition KeeperFSM[] = { * have to fetch the new value for synchronous_standby_names from the * monitor. */ - { PRIMARY_STATE, APPLY_SETTINGS_STATE, NODE_KIND_ANY, - COMMENT_PRIMARY_TO_APPLY_SETTINGS, - NULL }, + { + PRIMARY_STATE, APPLY_SETTINGS_STATE, NODE_KIND_ANY, + COMMENT_PRIMARY_TO_APPLY_SETTINGS, + NULL + }, - { WAIT_PRIMARY_STATE, APPLY_SETTINGS_STATE, NODE_KIND_ANY, - COMMENT_PRIMARY_TO_APPLY_SETTINGS, - NULL }, + { + WAIT_PRIMARY_STATE, APPLY_SETTINGS_STATE, NODE_KIND_ANY, + COMMENT_PRIMARY_TO_APPLY_SETTINGS, + NULL + }, - { APPLY_SETTINGS_STATE, PRIMARY_STATE, NODE_KIND_ANY, - COMMENT_APPLY_SETTINGS_TO_PRIMARY, - &fsm_enable_sync_rep }, + { + APPLY_SETTINGS_STATE, PRIMARY_STATE, NODE_KIND_ANY, + COMMENT_APPLY_SETTINGS_TO_PRIMARY, + &fsm_enable_sync_rep + }, - { APPLY_SETTINGS_STATE, SINGLE_STATE, NODE_KIND_ANY, - COMMENT_PRIMARY_TO_SINGLE, - &fsm_disable_replication }, + { + APPLY_SETTINGS_STATE, SINGLE_STATE, NODE_KIND_ANY, + COMMENT_PRIMARY_TO_SINGLE, + &fsm_disable_replication + }, - { APPLY_SETTINGS_STATE, WAIT_PRIMARY_STATE, NODE_KIND_ANY, - COMMENT_PRIMARY_TO_WAIT_PRIMARY, - &fsm_disable_sync_rep }, + { + APPLY_SETTINGS_STATE, WAIT_PRIMARY_STATE, NODE_KIND_ANY, + COMMENT_PRIMARY_TO_WAIT_PRIMARY, + &fsm_disable_sync_rep + }, - { APPLY_SETTINGS_STATE, JOIN_PRIMARY_STATE, NODE_KIND_ANY, - COMMENT_PRIMARY_TO_JOIN_PRIMARY, - &fsm_prepare_replication }, + { + APPLY_SETTINGS_STATE, JOIN_PRIMARY_STATE, NODE_KIND_ANY, + COMMENT_PRIMARY_TO_JOIN_PRIMARY, + &fsm_prepare_replication + }, /* * In case of multiple standbys, failover begins with reporting current LSN */ - { SECONDARY_STATE, REPORT_LSN_STATE, NODE_KIND_ANY, - COMMENT_SECONDARY_TO_REPORT_LSN, - &fsm_report_lsn }, + { + SECONDARY_STATE, REPORT_LSN_STATE, NODE_KIND_ANY, + COMMENT_SECONDARY_TO_REPORT_LSN, + &fsm_report_lsn + }, - { CATCHINGUP_STATE, REPORT_LSN_STATE, NODE_KIND_ANY, - COMMENT_SECONDARY_TO_REPORT_LSN, - &fsm_report_lsn }, + { + CATCHINGUP_STATE, REPORT_LSN_STATE, NODE_KIND_ANY, + COMMENT_SECONDARY_TO_REPORT_LSN, + &fsm_report_lsn + }, - { MAINTENANCE_STATE, REPORT_LSN_STATE, NODE_KIND_ANY, - COMMENT_SECONDARY_TO_REPORT_LSN, - &fsm_report_lsn }, + { + MAINTENANCE_STATE, REPORT_LSN_STATE, NODE_KIND_ANY, + COMMENT_SECONDARY_TO_REPORT_LSN, + &fsm_report_lsn + }, - { PREPARE_MAINTENANCE_STATE, REPORT_LSN_STATE, NODE_KIND_ANY, - COMMENT_SECONDARY_TO_REPORT_LSN, - &fsm_report_lsn }, + { + PREPARE_MAINTENANCE_STATE, REPORT_LSN_STATE, NODE_KIND_ANY, + COMMENT_SECONDARY_TO_REPORT_LSN, + &fsm_report_lsn + }, - { REPORT_LSN_STATE, PREP_PROMOTION_STATE, NODE_KIND_CITUS_WORKER, - COMMENT_REPORT_LSN_TO_PREP_PROMOTION, - &fsm_citus_worker_prepare_standby_for_promotion }, + { + REPORT_LSN_STATE, PREP_PROMOTION_STATE, NODE_KIND_CITUS_WORKER, + COMMENT_REPORT_LSN_TO_PREP_PROMOTION, + &fsm_citus_worker_prepare_standby_for_promotion + }, - { REPORT_LSN_STATE, PREP_PROMOTION_STATE, NODE_KIND_ANY, - COMMENT_REPORT_LSN_TO_PREP_PROMOTION, - &fsm_prepare_standby_for_promotion }, + { + REPORT_LSN_STATE, PREP_PROMOTION_STATE, NODE_KIND_ANY, + COMMENT_REPORT_LSN_TO_PREP_PROMOTION, + &fsm_prepare_standby_for_promotion + }, - { REPORT_LSN_STATE, FAST_FORWARD_STATE, NODE_KIND_ANY, - COMMENT_REPORT_LSN_TO_FAST_FORWARD, - &fsm_fast_forward }, + { + REPORT_LSN_STATE, FAST_FORWARD_STATE, NODE_KIND_ANY, + COMMENT_REPORT_LSN_TO_FAST_FORWARD, + &fsm_fast_forward + }, - { FAST_FORWARD_STATE, PREP_PROMOTION_STATE, NODE_KIND_CITUS_ANY, - COMMENT_FAST_FORWARD_TO_PREP_PROMOTION, - &fsm_citus_cleanup_and_resume_as_primary }, + { + FAST_FORWARD_STATE, PREP_PROMOTION_STATE, NODE_KIND_CITUS_ANY, + COMMENT_FAST_FORWARD_TO_PREP_PROMOTION, + &fsm_citus_cleanup_and_resume_as_primary + }, - { FAST_FORWARD_STATE, PREP_PROMOTION_STATE, NODE_KIND_ANY, - COMMENT_FAST_FORWARD_TO_PREP_PROMOTION, - &fsm_cleanup_as_primary }, + { + FAST_FORWARD_STATE, PREP_PROMOTION_STATE, NODE_KIND_ANY, + COMMENT_FAST_FORWARD_TO_PREP_PROMOTION, + &fsm_cleanup_as_primary + }, - { REPORT_LSN_STATE, JOIN_SECONDARY_STATE, NODE_KIND_ANY, - COMMENT_REPORT_LSN_TO_JOIN_SECONDARY, - &fsm_checkpoint_and_stop_postgres }, + { + REPORT_LSN_STATE, JOIN_SECONDARY_STATE, NODE_KIND_ANY, + COMMENT_REPORT_LSN_TO_JOIN_SECONDARY, + &fsm_checkpoint_and_stop_postgres + }, - { REPORT_LSN_STATE, SECONDARY_STATE, NODE_KIND_ANY, - COMMENT_REPORT_LSN_TO_JOIN_SECONDARY, - &fsm_follow_new_primary }, + { + REPORT_LSN_STATE, SECONDARY_STATE, NODE_KIND_ANY, + COMMENT_REPORT_LSN_TO_JOIN_SECONDARY, + &fsm_follow_new_primary + }, - { JOIN_SECONDARY_STATE, SECONDARY_STATE, NODE_KIND_ANY, - COMMENT_JOIN_SECONDARY_TO_SECONDARY, - &fsm_follow_new_primary }, + { + JOIN_SECONDARY_STATE, SECONDARY_STATE, NODE_KIND_ANY, + COMMENT_JOIN_SECONDARY_TO_SECONDARY, + &fsm_follow_new_primary + }, /* * When an old primary gets back online and reaches draining/draining, if a * failover is on-going then have it join the selection process. */ - { DRAINING_STATE, REPORT_LSN_STATE, NODE_KIND_ANY, - COMMENT_DRAINING_TO_REPORT_LSN, - &fsm_report_lsn_and_drop_replication_slots }, + { + DRAINING_STATE, REPORT_LSN_STATE, NODE_KIND_ANY, + COMMENT_DRAINING_TO_REPORT_LSN, + &fsm_report_lsn_and_drop_replication_slots + }, - { DEMOTED_STATE, REPORT_LSN_STATE, NODE_KIND_ANY, - COMMENT_DEMOTED_TO_REPORT_LSN, - &fsm_report_lsn_and_drop_replication_slots }, + { + DEMOTED_STATE, REPORT_LSN_STATE, NODE_KIND_ANY, + COMMENT_DEMOTED_TO_REPORT_LSN, + &fsm_report_lsn_and_drop_replication_slots + }, /* * When adding a new node and there is no primary, but there are existing * nodes that are not candidates for failover. */ - { INIT_STATE, REPORT_LSN_STATE, NODE_KIND_ANY, - COMMENT_INIT_TO_REPORT_LSN, - &fsm_init_from_standby }, + { + INIT_STATE, REPORT_LSN_STATE, NODE_KIND_ANY, + COMMENT_INIT_TO_REPORT_LSN, + &fsm_init_from_standby + }, /* * Dropping a node is a two-step process */ - { ANY_STATE, DROPPED_STATE, NODE_KIND_CITUS_ANY, - COMMENT_ANY_TO_DROPPED, - &fsm_citus_drop_node }, + { + ANY_STATE, DROPPED_STATE, NODE_KIND_CITUS_ANY, + COMMENT_ANY_TO_DROPPED, + &fsm_citus_drop_node + }, - { ANY_STATE, DROPPED_STATE, NODE_KIND_ANY, - COMMENT_ANY_TO_DROPPED, - &fsm_drop_node }, + { + ANY_STATE, DROPPED_STATE, NODE_KIND_ANY, + COMMENT_ANY_TO_DROPPED, + &fsm_drop_node + }, /* * This is the end, my friend. */ - { NO_STATE, NO_STATE, NODE_KIND_ANY, - NULL, - NULL }, + { + NO_STATE, NO_STATE, NODE_KIND_ANY, + NULL, + NULL + }, }; diff --git a/src/bin/pg_autoctl/keeper.c b/src/bin/pg_autoctl/keeper.c index 6315cd00b..5b8083ac1 100644 --- a/src/bin/pg_autoctl/keeper.c +++ b/src/bin/pg_autoctl/keeper.c @@ -312,9 +312,10 @@ keeper_ensure_current_state(Keeper *keeper) case MAINTENANCE_STATE: default: - + { /* nothing to be done here */ return true; + } } /* should never happen */ @@ -387,7 +388,7 @@ ReportPgIsRunning(Keeper *keeper) */ log_error("Failed to restart PostgreSQL %d times in the " "last %" PRIu64 "s, reporting PostgreSQL not running to " - "the pg_auto_failover monitor.", + "the pg_auto_failover monitor.", postgres->pgStartRetries, now - postgres->pgFirstStartFailureTs); @@ -637,7 +638,7 @@ keeper_state_check_postgres(Keeper *keeper, PostgresControlData *control) * are doing anymore. */ log_error("Unknown PostgreSQL system identifier: %" PRIu64 ", " - "expected %" PRIu64, + "expected %" PRIu64, keeperState->system_identifier, control->system_identifier); return false; @@ -2739,8 +2740,8 @@ keeper_config_accept_new(Keeper *keeper, KeeperConfig *newConfig) newConfig->prepare_promotion_walreceiver; } - if (newConfig->postgresql_restart_failure_timeout != - config->postgresql_restart_failure_timeout) + if (newConfig->postgresql_restart_failure_timeout != config-> + postgresql_restart_failure_timeout) { log_info( "Reloading configuration: timeout.postgresql_restart_failure_timeout " @@ -2752,8 +2753,8 @@ keeper_config_accept_new(Keeper *keeper, KeeperConfig *newConfig) newConfig->postgresql_restart_failure_timeout; } - if (newConfig->postgresql_restart_failure_max_retries != - config->postgresql_restart_failure_max_retries) + if (newConfig->postgresql_restart_failure_max_retries != config-> + postgresql_restart_failure_max_retries) { log_info( "Reloading configuration: retries.postgresql_restart_failure_max_retries " diff --git a/src/bin/pg_autoctl/keeper_config.c b/src/bin/pg_autoctl/keeper_config.c index 1459336df..2cdcf74bf 100644 --- a/src/bin/pg_autoctl/keeper_config.c +++ b/src/bin/pg_autoctl/keeper_config.c @@ -22,25 +22,25 @@ #include "pgctl.h" #define OPTION_AUTOCTL_ROLE(config) \ - make_strbuf_option_default("pg_autoctl", "role", NULL, true, NAMEDATALEN, \ - config->role, KEEPER_ROLE) + make_strbuf_option_default("pg_autoctl", "role", NULL, true, NAMEDATALEN, \ + config->role, KEEPER_ROLE) #define OPTION_AUTOCTL_MONITOR(config) \ - make_strbuf_option("pg_autoctl", "monitor", "monitor", false, MAXCONNINFO, \ - config->monitor_pguri) + make_strbuf_option("pg_autoctl", "monitor", "monitor", false, MAXCONNINFO, \ + config->monitor_pguri) #define OPTION_AUTOCTL_FORMATION(config) \ - make_strbuf_option_default("pg_autoctl", "formation", "formation", \ - true, NAMEDATALEN, \ - config->formation, FORMATION_DEFAULT) + make_strbuf_option_default("pg_autoctl", "formation", "formation", \ + true, NAMEDATALEN, \ + config->formation, FORMATION_DEFAULT) #define OPTION_AUTOCTL_GROUPID(config) \ - make_int_option("pg_autoctl", "group", "group", false, &(config->groupId)) + make_int_option("pg_autoctl", "group", "group", false, &(config->groupId)) #define OPTION_AUTOCTL_NAME(config) \ - make_strbuf_option_default("pg_autoctl", "name", "name", \ - false, _POSIX_HOST_NAME_MAX, \ - config->name, "") + make_strbuf_option_default("pg_autoctl", "name", "name", \ + false, _POSIX_HOST_NAME_MAX, \ + config->name, "") /* * --hostname used to be --nodename, and we need to support transition from the @@ -50,212 +50,212 @@ * As a result HOSTNAME is marked not required and NODENAME is marked compat. */ #define OPTION_AUTOCTL_HOSTNAME(config) \ - make_strbuf_option("pg_autoctl", "hostname", "hostname", \ - false, _POSIX_HOST_NAME_MAX, config->hostname) + make_strbuf_option("pg_autoctl", "hostname", "hostname", \ + false, _POSIX_HOST_NAME_MAX, config->hostname) #define OPTION_AUTOCTL_NODENAME(config) \ - make_strbuf_compat_option("pg_autoctl", "nodename", \ - _POSIX_HOST_NAME_MAX, config->hostname) + make_strbuf_compat_option("pg_autoctl", "nodename", \ + _POSIX_HOST_NAME_MAX, config->hostname) #define OPTION_AUTOCTL_NODEKIND(config) \ - make_strbuf_option("pg_autoctl", "nodekind", NULL, false, NAMEDATALEN, \ - config->nodeKind) + make_strbuf_option("pg_autoctl", "nodekind", NULL, false, NAMEDATALEN, \ + config->nodeKind) #define OPTION_POSTGRESQL_PGDATA(config) \ - make_strbuf_option("postgresql", "pgdata", "pgdata", true, MAXPGPATH, \ - config->pgSetup.pgdata) + make_strbuf_option("postgresql", "pgdata", "pgdata", true, MAXPGPATH, \ + config->pgSetup.pgdata) #define OPTION_POSTGRESQL_PG_CTL(config) \ - make_strbuf_option("postgresql", "pg_ctl", "pgctl", false, MAXPGPATH, \ - config->pgSetup.pg_ctl) + make_strbuf_option("postgresql", "pg_ctl", "pgctl", false, MAXPGPATH, \ + config->pgSetup.pg_ctl) #define OPTION_POSTGRESQL_USERNAME(config) \ - make_strbuf_option("postgresql", "username", "username", \ - false, NAMEDATALEN, \ - config->pgSetup.username) + make_strbuf_option("postgresql", "username", "username", \ + false, NAMEDATALEN, \ + config->pgSetup.username) #define OPTION_POSTGRESQL_DBNAME(config) \ - make_strbuf_option("postgresql", "dbname", "dbname", false, NAMEDATALEN, \ - config->pgSetup.dbname) + make_strbuf_option("postgresql", "dbname", "dbname", false, NAMEDATALEN, \ + config->pgSetup.dbname) #define OPTION_POSTGRESQL_HOST(config) \ - make_strbuf_option("postgresql", "host", "pghost", \ - false, _POSIX_HOST_NAME_MAX, \ - config->pgSetup.pghost) + make_strbuf_option("postgresql", "host", "pghost", \ + false, _POSIX_HOST_NAME_MAX, \ + config->pgSetup.pghost) #define OPTION_POSTGRESQL_PORT(config) \ - make_int_option("postgresql", "port", "pgport", \ - true, &(config->pgSetup.pgport)) + make_int_option("postgresql", "port", "pgport", \ + true, &(config->pgSetup.pgport)) #define OPTION_POSTGRESQL_PROXY_PORT(config) \ - make_int_option("postgresql", "proxyport", "proxyport", \ - false, &(config->pgSetup.proxyport)) + make_int_option("postgresql", "proxyport", "proxyport", \ + false, &(config->pgSetup.proxyport)) #define OPTION_POSTGRESQL_LISTEN_ADDRESSES(config) \ - make_strbuf_option("postgresql", "listen_addresses", "listen", \ - false, MAXPGPATH, config->pgSetup.listen_addresses) + make_strbuf_option("postgresql", "listen_addresses", "listen", \ + false, MAXPGPATH, config->pgSetup.listen_addresses) #define OPTION_POSTGRESQL_AUTH_METHOD(config) \ - make_strbuf_option("postgresql", "auth_method", "auth", \ - false, MAXPGPATH, config->pgSetup.authMethod) + make_strbuf_option("postgresql", "auth_method", "auth", \ + false, MAXPGPATH, config->pgSetup.authMethod) #define OPTION_POSTGRESQL_HBA_LEVEL(config) \ - make_strbuf_option("postgresql", "hba_level", NULL, \ - false, MAXPGPATH, config->pgSetup.hbaLevelStr) + make_strbuf_option("postgresql", "hba_level", NULL, \ + false, MAXPGPATH, config->pgSetup.hbaLevelStr) #define OPTION_SSL_ACTIVE(config) \ - make_int_option_default("ssl", "active", NULL, \ - false, &(config->pgSetup.ssl.active), 0) + make_int_option_default("ssl", "active", NULL, \ + false, &(config->pgSetup.ssl.active), 0) #define OPTION_SSL_MODE(config) \ - make_strbuf_option("ssl", "sslmode", "ssl-mode", \ - false, SSL_MODE_STRLEN, config->pgSetup.ssl.sslModeStr) + make_strbuf_option("ssl", "sslmode", "ssl-mode", \ + false, SSL_MODE_STRLEN, config->pgSetup.ssl.sslModeStr) #define OPTION_SSL_CA_FILE(config) \ - make_strbuf_option("ssl", "ca_file", "ssl-ca-file", \ - false, MAXPGPATH, config->pgSetup.ssl.caFile) + make_strbuf_option("ssl", "ca_file", "ssl-ca-file", \ + false, MAXPGPATH, config->pgSetup.ssl.caFile) #define OPTION_SSL_CRL_FILE(config) \ - make_strbuf_option("ssl", "crl_file", "ssl-crl-file", \ - false, MAXPGPATH, config->pgSetup.ssl.crlFile) + make_strbuf_option("ssl", "crl_file", "ssl-crl-file", \ + false, MAXPGPATH, config->pgSetup.ssl.crlFile) #define OPTION_SSL_SERVER_CERT(config) \ - make_strbuf_option("ssl", "cert_file", "server-cert", \ - false, MAXPGPATH, config->pgSetup.ssl.serverCert) + make_strbuf_option("ssl", "cert_file", "server-cert", \ + false, MAXPGPATH, config->pgSetup.ssl.serverCert) #define OPTION_SSL_SERVER_KEY(config) \ - make_strbuf_option("ssl", "key_file", "server-key", \ - false, MAXPGPATH, config->pgSetup.ssl.serverKey) + make_strbuf_option("ssl", "key_file", "server-key", \ + false, MAXPGPATH, config->pgSetup.ssl.serverKey) #define OPTION_REPLICATION_PASSWORD(config) \ - make_strbuf_option_default("replication", "password", NULL, \ - false, MAXCONNINFO, \ - config->replication_password, \ - REPLICATION_PASSWORD_DEFAULT) + make_strbuf_option_default("replication", "password", NULL, \ + false, MAXCONNINFO, \ + config->replication_password, \ + REPLICATION_PASSWORD_DEFAULT) #define OPTION_REPLICATION_MAXIMUM_BACKUP_RATE(config) \ - make_strbuf_option_default("replication", "maximum_backup_rate", NULL, \ - false, MAXIMUM_BACKUP_RATE_LEN, \ - config->maximum_backup_rate, \ - MAXIMUM_BACKUP_RATE) + make_strbuf_option_default("replication", "maximum_backup_rate", NULL, \ + false, MAXIMUM_BACKUP_RATE_LEN, \ + config->maximum_backup_rate, \ + MAXIMUM_BACKUP_RATE) #define OPTION_REPLICATION_BACKUP_DIR(config) \ - make_strbuf_option("replication", "backup_directory", NULL, \ - false, MAXPGPATH, config->backupDirectory) + make_strbuf_option("replication", "backup_directory", NULL, \ + false, MAXPGPATH, config->backupDirectory) #define OPTION_TIMEOUT_NETWORK_PARTITION(config) \ - make_int_option_default("timeout", "network_partition_timeout", \ - NULL, false, \ - &(config->network_partition_timeout), \ - NETWORK_PARTITION_TIMEOUT) + make_int_option_default("timeout", "network_partition_timeout", \ + NULL, false, \ + &(config->network_partition_timeout), \ + NETWORK_PARTITION_TIMEOUT) #define OPTION_TIMEOUT_PREPARE_PROMOTION_CATCHUP(config) \ - make_int_option_default("timeout", "prepare_promotion_catchup", \ - NULL, \ - false, \ - &(config->prepare_promotion_catchup), \ - PREPARE_PROMOTION_CATCHUP_TIMEOUT) + make_int_option_default("timeout", "prepare_promotion_catchup", \ + NULL, \ + false, \ + &(config->prepare_promotion_catchup), \ + PREPARE_PROMOTION_CATCHUP_TIMEOUT) #define OPTION_TIMEOUT_PREPARE_PROMOTION_WALRECEIVER(config) \ - make_int_option_default("timeout", "prepare_promotion_walreceiver", \ - NULL, \ - false, \ - &(config->prepare_promotion_walreceiver), \ - PREPARE_PROMOTION_WALRECEIVER_TIMEOUT) + make_int_option_default("timeout", "prepare_promotion_walreceiver", \ + NULL, \ + false, \ + &(config->prepare_promotion_walreceiver), \ + PREPARE_PROMOTION_WALRECEIVER_TIMEOUT) #define OPTION_TIMEOUT_POSTGRESQL_RESTART_FAILURE_TIMEOUT(config) \ - make_int_option_default("timeout", "postgresql_restart_failure_timeout", \ - NULL, \ - false, \ - &(config->postgresql_restart_failure_timeout), \ - POSTGRESQL_FAILS_TO_START_TIMEOUT) + make_int_option_default("timeout", "postgresql_restart_failure_timeout", \ + NULL, \ + false, \ + &(config->postgresql_restart_failure_timeout), \ + POSTGRESQL_FAILS_TO_START_TIMEOUT) #define OPTION_TIMEOUT_POSTGRESQL_RESTART_FAILURE_MAX_RETRIES(config) \ - make_int_option_default("timeout", "postgresql_restart_failure_max_retries", \ - NULL, \ - false, \ - &(config->postgresql_restart_failure_max_retries), \ - POSTGRESQL_FAILS_TO_START_RETRIES) + make_int_option_default("timeout", "postgresql_restart_failure_max_retries", \ + NULL, \ + false, \ + &(config->postgresql_restart_failure_max_retries), \ + POSTGRESQL_FAILS_TO_START_RETRIES) #define OPTION_TIMEOUT_CITUS_MASTER_UPDATE_NODE_LOCK_COOLDOWN(config) \ - make_int_option_default("timeout", "citus_master_update_node_lock_cooldown", \ - NULL, \ - false, \ - &(config->citus_master_update_node_lock_cooldown), \ - CITUS_MASTER_UPDATE_NODE_LOCK_COOLDOWN) + make_int_option_default("timeout", "citus_master_update_node_lock_cooldown", \ + NULL, \ + false, \ + &(config->citus_master_update_node_lock_cooldown), \ + CITUS_MASTER_UPDATE_NODE_LOCK_COOLDOWN) #define OPTION_TIMEOUT_CITUS_COORDINATOR_WAIT_MAX_RETRIES(config) \ - make_int_option_default("timeout", "citus_coordinator_wait_max_retries", \ - NULL, \ - false, \ - &(config->citus_coordinator_wait_max_retries), \ - CITUS_COORDINATOR_WAIT_MAX_RETRIES) + make_int_option_default("timeout", "citus_coordinator_wait_max_retries", \ + NULL, \ + false, \ + &(config->citus_coordinator_wait_max_retries), \ + CITUS_COORDINATOR_WAIT_MAX_RETRIES) #define OPTION_TIMEOUT_CITUS_COORDINATOR_WAIT_TIMEOUT(config) \ - make_int_option_default("timeout", "citus_coordinator_wait_timeout", \ - NULL, \ - false, \ - &(config->citus_coordinator_wait_timeout), \ - CITUS_COORDINATOR_WAIT_TIMEOUT) + make_int_option_default("timeout", "citus_coordinator_wait_timeout", \ + NULL, \ + false, \ + &(config->citus_coordinator_wait_timeout), \ + CITUS_COORDINATOR_WAIT_TIMEOUT) #define OPTION_TIMEOUT_LISTEN_NOTIFICATIONS(config) \ - make_int_option_default("timeout", "listen_notifications_timeout", \ - NULL, false, \ - &(config->listen_notifications_timeout), \ - PG_AUTOCTL_LISTEN_NOTIFICATIONS_TIMEOUT) + make_int_option_default("timeout", "listen_notifications_timeout", \ + NULL, false, \ + &(config->listen_notifications_timeout), \ + PG_AUTOCTL_LISTEN_NOTIFICATIONS_TIMEOUT) #define OPTION_CITUS_ROLE(config) \ - make_strbuf_option_default("citus", "role", NULL, false, NAMEDATALEN, \ - config->citusRoleStr, DEFAULT_CITUS_ROLE) + make_strbuf_option_default("citus", "role", NULL, false, NAMEDATALEN, \ + config->citusRoleStr, DEFAULT_CITUS_ROLE) #define OPTION_CITUS_CLUSTER_NAME(config) \ - make_strbuf_option("citus", "cluster_name", "citus-cluster", \ - false, NAMEDATALEN, config->pgSetup.citusClusterName) + make_strbuf_option("citus", "cluster_name", "citus-cluster", \ + false, NAMEDATALEN, config->pgSetup.citusClusterName) #define SET_INI_OPTIONS_ARRAY(config) \ - { \ - OPTION_AUTOCTL_ROLE(config), \ - OPTION_AUTOCTL_MONITOR(config), \ - OPTION_AUTOCTL_FORMATION(config), \ - OPTION_AUTOCTL_GROUPID(config), \ - OPTION_AUTOCTL_NAME(config), \ - OPTION_AUTOCTL_HOSTNAME(config), \ - OPTION_AUTOCTL_NODENAME(config), \ - OPTION_AUTOCTL_NODEKIND(config), \ - OPTION_POSTGRESQL_PGDATA(config), \ - OPTION_POSTGRESQL_PG_CTL(config), \ - OPTION_POSTGRESQL_USERNAME(config), \ - OPTION_POSTGRESQL_DBNAME(config), \ - OPTION_POSTGRESQL_HOST(config), \ - OPTION_POSTGRESQL_PORT(config), \ - OPTION_POSTGRESQL_PROXY_PORT(config), \ - OPTION_POSTGRESQL_LISTEN_ADDRESSES(config), \ - OPTION_POSTGRESQL_AUTH_METHOD(config), \ - OPTION_POSTGRESQL_HBA_LEVEL(config), \ - OPTION_SSL_ACTIVE(config), \ - OPTION_SSL_MODE(config), \ - OPTION_SSL_CA_FILE(config), \ - OPTION_SSL_CRL_FILE(config), \ - OPTION_SSL_SERVER_CERT(config), \ - OPTION_SSL_SERVER_KEY(config), \ - OPTION_REPLICATION_MAXIMUM_BACKUP_RATE(config), \ - OPTION_REPLICATION_BACKUP_DIR(config), \ - OPTION_REPLICATION_PASSWORD(config), \ - OPTION_TIMEOUT_NETWORK_PARTITION(config), \ - OPTION_TIMEOUT_PREPARE_PROMOTION_CATCHUP(config), \ - OPTION_TIMEOUT_PREPARE_PROMOTION_WALRECEIVER(config), \ - OPTION_TIMEOUT_POSTGRESQL_RESTART_FAILURE_TIMEOUT(config), \ - OPTION_TIMEOUT_POSTGRESQL_RESTART_FAILURE_MAX_RETRIES(config), \ - OPTION_TIMEOUT_LISTEN_NOTIFICATIONS(config), \ + { \ + OPTION_AUTOCTL_ROLE(config), \ + OPTION_AUTOCTL_MONITOR(config), \ + OPTION_AUTOCTL_FORMATION(config), \ + OPTION_AUTOCTL_GROUPID(config), \ + OPTION_AUTOCTL_NAME(config), \ + OPTION_AUTOCTL_HOSTNAME(config), \ + OPTION_AUTOCTL_NODENAME(config), \ + OPTION_AUTOCTL_NODEKIND(config), \ + OPTION_POSTGRESQL_PGDATA(config), \ + OPTION_POSTGRESQL_PG_CTL(config), \ + OPTION_POSTGRESQL_USERNAME(config), \ + OPTION_POSTGRESQL_DBNAME(config), \ + OPTION_POSTGRESQL_HOST(config), \ + OPTION_POSTGRESQL_PORT(config), \ + OPTION_POSTGRESQL_PROXY_PORT(config), \ + OPTION_POSTGRESQL_LISTEN_ADDRESSES(config), \ + OPTION_POSTGRESQL_AUTH_METHOD(config), \ + OPTION_POSTGRESQL_HBA_LEVEL(config), \ + OPTION_SSL_ACTIVE(config), \ + OPTION_SSL_MODE(config), \ + OPTION_SSL_CA_FILE(config), \ + OPTION_SSL_CRL_FILE(config), \ + OPTION_SSL_SERVER_CERT(config), \ + OPTION_SSL_SERVER_KEY(config), \ + OPTION_REPLICATION_MAXIMUM_BACKUP_RATE(config), \ + OPTION_REPLICATION_BACKUP_DIR(config), \ + OPTION_REPLICATION_PASSWORD(config), \ + OPTION_TIMEOUT_NETWORK_PARTITION(config), \ + OPTION_TIMEOUT_PREPARE_PROMOTION_CATCHUP(config), \ + OPTION_TIMEOUT_PREPARE_PROMOTION_WALRECEIVER(config), \ + OPTION_TIMEOUT_POSTGRESQL_RESTART_FAILURE_TIMEOUT(config), \ + OPTION_TIMEOUT_POSTGRESQL_RESTART_FAILURE_MAX_RETRIES(config), \ + OPTION_TIMEOUT_LISTEN_NOTIFICATIONS(config), \ \ - OPTION_TIMEOUT_CITUS_MASTER_UPDATE_NODE_LOCK_COOLDOWN(config), \ - OPTION_TIMEOUT_CITUS_COORDINATOR_WAIT_MAX_RETRIES(config), \ - OPTION_TIMEOUT_CITUS_COORDINATOR_WAIT_TIMEOUT(config), \ + OPTION_TIMEOUT_CITUS_MASTER_UPDATE_NODE_LOCK_COOLDOWN(config), \ + OPTION_TIMEOUT_CITUS_COORDINATOR_WAIT_MAX_RETRIES(config), \ + OPTION_TIMEOUT_CITUS_COORDINATOR_WAIT_TIMEOUT(config), \ \ - OPTION_CITUS_ROLE(config), \ - OPTION_CITUS_CLUSTER_NAME(config), \ - INI_OPTION_LAST \ - } + OPTION_CITUS_ROLE(config), \ + OPTION_CITUS_CLUSTER_NAME(config), \ + INI_OPTION_LAST \ + } static bool keeper_config_init_nodekind(KeeperConfig *config); static bool keeper_config_init_hbalevel(KeeperConfig *config); diff --git a/src/bin/pg_autoctl/keeper_config.h b/src/bin/pg_autoctl/keeper_config.h index 3826923d9..94d417a2d 100644 --- a/src/bin/pg_autoctl/keeper_config.h +++ b/src/bin/pg_autoctl/keeper_config.h @@ -74,7 +74,7 @@ typedef struct KeeperConfig } KeeperConfig; #define PG_AUTOCTL_MONITOR_IS_DISABLED(config) \ - (strcmp(config->monitor_pguri, PG_AUTOCTL_MONITOR_DISABLED) == 0) + (strcmp(config->monitor_pguri, PG_AUTOCTL_MONITOR_DISABLED) == 0) bool keeper_config_set_pathnames_from_pgdata(ConfigFilePaths *pathnames, const char *pgdata); diff --git a/src/bin/pg_autoctl/keeper_pg_init.c b/src/bin/pg_autoctl/keeper_pg_init.c index 3ff3e555e..043d589f7 100644 --- a/src/bin/pg_autoctl/keeper_pg_init.c +++ b/src/bin/pg_autoctl/keeper_pg_init.c @@ -566,11 +566,12 @@ reach_initial_state(Keeper *keeper) } default: - + { /* we don't support any other state at initialization time */ log_error("reach_initial_state: don't know how to read state %s", NodeStateToString(keeper->state.assigned_role)); return false; + } } /* diff --git a/src/bin/pg_autoctl/monitor_config.c b/src/bin/pg_autoctl/monitor_config.c index f62be0d8b..23611c6d9 100644 --- a/src/bin/pg_autoctl/monitor_config.c +++ b/src/bin/pg_autoctl/monitor_config.c @@ -22,8 +22,8 @@ #include "pgctl.h" #define OPTION_AUTOCTL_ROLE(config) \ - make_strbuf_option_default("pg_autoctl", "role", NULL, true, NAMEDATALEN, \ - config->role, MONITOR_ROLE) + make_strbuf_option_default("pg_autoctl", "role", NULL, true, NAMEDATALEN, \ + config->role, MONITOR_ROLE) /* * --hostname used to be --nodename, and we need to support transition from the @@ -33,96 +33,96 @@ * As a result HOSTNAME is marked not required and NODENAME is marked compat. */ #define OPTION_AUTOCTL_HOSTNAME(config) \ - make_strbuf_option("pg_autoctl", "hostname", "hostname", \ - false, _POSIX_HOST_NAME_MAX, config->hostname) + make_strbuf_option("pg_autoctl", "hostname", "hostname", \ + false, _POSIX_HOST_NAME_MAX, config->hostname) #define OPTION_AUTOCTL_NODENAME(config) \ - make_strbuf_compat_option("pg_autoctl", "nodename", \ - _POSIX_HOST_NAME_MAX, config->hostname) + make_strbuf_compat_option("pg_autoctl", "nodename", \ + _POSIX_HOST_NAME_MAX, config->hostname) #define OPTION_POSTGRESQL_PGDATA(config) \ - make_strbuf_option("postgresql", "pgdata", "pgdata", true, MAXPGPATH, \ - config->pgSetup.pgdata) + make_strbuf_option("postgresql", "pgdata", "pgdata", true, MAXPGPATH, \ + config->pgSetup.pgdata) #define OPTION_POSTGRESQL_PG_CTL(config) \ - make_strbuf_option("postgresql", "pg_ctl", "pgctl", false, MAXPGPATH, \ - config->pgSetup.pg_ctl) + make_strbuf_option("postgresql", "pg_ctl", "pgctl", false, MAXPGPATH, \ + config->pgSetup.pg_ctl) #define OPTION_POSTGRESQL_USERNAME(config) \ - make_strbuf_option("postgresql", "username", "username", \ - false, NAMEDATALEN, \ - config->pgSetup.username) + make_strbuf_option("postgresql", "username", "username", \ + false, NAMEDATALEN, \ + config->pgSetup.username) #define OPTION_POSTGRESQL_DBNAME(config) \ - make_strbuf_option("postgresql", "dbname", "dbname", false, NAMEDATALEN, \ - config->pgSetup.dbname) + make_strbuf_option("postgresql", "dbname", "dbname", false, NAMEDATALEN, \ + config->pgSetup.dbname) #define OPTION_POSTGRESQL_HOST(config) \ - make_strbuf_option("postgresql", "host", "pghost", \ - false, _POSIX_HOST_NAME_MAX, \ - config->pgSetup.pghost) + make_strbuf_option("postgresql", "host", "pghost", \ + false, _POSIX_HOST_NAME_MAX, \ + config->pgSetup.pghost) #define OPTION_POSTGRESQL_PORT(config) \ - make_int_option("postgresql", "port", "pgport", \ - true, &(config->pgSetup.pgport)) + make_int_option("postgresql", "port", "pgport", \ + true, &(config->pgSetup.pgport)) #define OPTION_POSTGRESQL_LISTEN_ADDRESSES(config) \ - make_strbuf_option("postgresql", "listen_addresses", "listen", \ - false, MAXPGPATH, config->pgSetup.listen_addresses) + make_strbuf_option("postgresql", "listen_addresses", "listen", \ + false, MAXPGPATH, config->pgSetup.listen_addresses) #define OPTION_POSTGRESQL_AUTH_METHOD(config) \ - make_strbuf_option("postgresql", "auth_method", "auth", \ - false, MAXPGPATH, config->pgSetup.authMethod) + make_strbuf_option("postgresql", "auth_method", "auth", \ + false, MAXPGPATH, config->pgSetup.authMethod) #define OPTION_SSL_ACTIVE(config) \ - make_int_option_default("ssl", "active", NULL, \ - false, &(config->pgSetup.ssl.active), 0) + make_int_option_default("ssl", "active", NULL, \ + false, &(config->pgSetup.ssl.active), 0) #define OPTION_SSL_MODE(config) \ - make_strbuf_option("ssl", "sslmode", "ssl-mode", \ - false, SSL_MODE_STRLEN, config->pgSetup.ssl.sslModeStr) + make_strbuf_option("ssl", "sslmode", "ssl-mode", \ + false, SSL_MODE_STRLEN, config->pgSetup.ssl.sslModeStr) #define OPTION_SSL_CA_FILE(config) \ - make_strbuf_option("ssl", "ca_file", "ssl-ca-file", \ - false, MAXPGPATH, config->pgSetup.ssl.caFile) + make_strbuf_option("ssl", "ca_file", "ssl-ca-file", \ + false, MAXPGPATH, config->pgSetup.ssl.caFile) #define OPTION_SSL_CRL_FILE(config) \ - make_strbuf_option("ssl", "crl_file", "ssl-crl-file", \ - false, MAXPGPATH, config->pgSetup.ssl.crlFile) + make_strbuf_option("ssl", "crl_file", "ssl-crl-file", \ + false, MAXPGPATH, config->pgSetup.ssl.crlFile) #define OPTION_SSL_SERVER_CERT(config) \ - make_strbuf_option("ssl", "cert_file", "server-cert", \ - false, MAXPGPATH, config->pgSetup.ssl.serverCert) + make_strbuf_option("ssl", "cert_file", "server-cert", \ + false, MAXPGPATH, config->pgSetup.ssl.serverCert) #define OPTION_SSL_SERVER_KEY(config) \ - make_strbuf_option("ssl", "key_file", "server-key", \ - false, MAXPGPATH, config->pgSetup.ssl.serverKey) + make_strbuf_option("ssl", "key_file", "server-key", \ + false, MAXPGPATH, config->pgSetup.ssl.serverKey) #define SET_INI_OPTIONS_ARRAY(config) \ - { \ - OPTION_AUTOCTL_ROLE(config), \ - OPTION_AUTOCTL_HOSTNAME(config), \ - OPTION_AUTOCTL_NODENAME(config), \ - OPTION_POSTGRESQL_PGDATA(config), \ - OPTION_POSTGRESQL_PG_CTL(config), \ - OPTION_POSTGRESQL_USERNAME(config), \ - OPTION_POSTGRESQL_DBNAME(config), \ - OPTION_POSTGRESQL_HOST(config), \ - OPTION_POSTGRESQL_PORT(config), \ - OPTION_POSTGRESQL_LISTEN_ADDRESSES(config), \ - OPTION_POSTGRESQL_AUTH_METHOD(config), \ - OPTION_SSL_MODE(config), \ - OPTION_SSL_ACTIVE(config), \ - OPTION_SSL_CA_FILE(config), \ - OPTION_SSL_CRL_FILE(config), \ - OPTION_SSL_SERVER_CERT(config), \ - OPTION_SSL_SERVER_KEY(config), \ - make_strbuf_option_default("pg_auto_failover", "autoctl_node_password", \ - NULL, false, MAXCONNINFO, \ - config->autoctl_node_password, ""), \ - INI_OPTION_LAST \ - } + { \ + OPTION_AUTOCTL_ROLE(config), \ + OPTION_AUTOCTL_HOSTNAME(config), \ + OPTION_AUTOCTL_NODENAME(config), \ + OPTION_POSTGRESQL_PGDATA(config), \ + OPTION_POSTGRESQL_PG_CTL(config), \ + OPTION_POSTGRESQL_USERNAME(config), \ + OPTION_POSTGRESQL_DBNAME(config), \ + OPTION_POSTGRESQL_HOST(config), \ + OPTION_POSTGRESQL_PORT(config), \ + OPTION_POSTGRESQL_LISTEN_ADDRESSES(config), \ + OPTION_POSTGRESQL_AUTH_METHOD(config), \ + OPTION_SSL_MODE(config), \ + OPTION_SSL_ACTIVE(config), \ + OPTION_SSL_CA_FILE(config), \ + OPTION_SSL_CRL_FILE(config), \ + OPTION_SSL_SERVER_CERT(config), \ + OPTION_SSL_SERVER_KEY(config), \ + make_strbuf_option_default("pg_auto_failover", "autoctl_node_password", \ + NULL, false, MAXCONNINFO, \ + config->autoctl_node_password, ""), \ + INI_OPTION_LAST \ + } /* diff --git a/src/bin/pg_autoctl/nodespec.c b/src/bin/pg_autoctl/nodespec.c index 5f61e6d91..cad52241c 100644 --- a/src/bin/pg_autoctl/nodespec.c +++ b/src/bin/pg_autoctl/nodespec.c @@ -458,11 +458,11 @@ nodespec_create_argv(const NodeSpec *spec, int i = 0; #define PUSH(v) do { \ - if (i >= args_size - 1) { \ - log_error("nodespec_create_argv: args[] overflow"); \ - return -1; \ - } \ - args[i++] = (char *) (v); \ + if (i >= args_size - 1) { \ + log_error("nodespec_create_argv: args[] overflow"); \ + return -1; \ + } \ + args[i++] = (char *) (v); \ } while (0) PUSH(pg_autoctl_path); diff --git a/src/bin/pg_autoctl/primary_standby.c b/src/bin/pg_autoctl/primary_standby.c index 96a171978..af2db8a35 100644 --- a/src/bin/pg_autoctl/primary_standby.c +++ b/src/bin/pg_autoctl/primary_standby.c @@ -39,39 +39,39 @@ static void local_postgres_update_pg_failures_tracking(LocalPostgresServer *post * replaced with dynamic values from the setup when used. */ #define DEFAULT_GUC_SETTINGS_FOR_PG_AUTO_FAILOVER \ - { "shared_preload_libraries", "pg_stat_statements" }, \ - { "listen_addresses", "'*'" }, \ - { "port", "5432" }, \ - { "max_wal_senders", "12" }, \ - { "max_replication_slots", "12" }, \ - { "wal_level", "'replica'" }, \ - { "wal_log_hints", "on" }, \ - { "wal_sender_timeout", "'30s'" }, \ - { "hot_standby_feedback", "on" }, \ - { "hot_standby", "on" }, \ - { "synchronous_commit", "on" }, \ - { "logging_collector", "on" }, \ - { "log_destination", "stderr" }, \ - { "log_directory", "log" }, \ - { "log_min_messages", "info" }, \ - { "log_connections", "off" }, \ - { "log_disconnections", "off" }, \ - { "log_lock_waits", "on" }, \ - { "password_encryption", "md5" }, \ - { "ssl", "off" }, \ - { "ssl_ca_file", "" }, \ - { "ssl_crl_file", "" }, \ - { "ssl_cert_file", "" }, \ - { "ssl_key_file", "" }, \ - { "ssl_ciphers", "'" DEFAULT_SSL_CIPHERS "'" } + { "shared_preload_libraries", "pg_stat_statements" }, \ + { "listen_addresses", "'*'" }, \ + { "port", "5432" }, \ + { "max_wal_senders", "12" }, \ + { "max_replication_slots", "12" }, \ + { "wal_level", "'replica'" }, \ + { "wal_log_hints", "on" }, \ + { "wal_sender_timeout", "'30s'" }, \ + { "hot_standby_feedback", "on" }, \ + { "hot_standby", "on" }, \ + { "synchronous_commit", "on" }, \ + { "logging_collector", "on" }, \ + { "log_destination", "stderr" }, \ + { "log_directory", "log" }, \ + { "log_min_messages", "info" }, \ + { "log_connections", "off" }, \ + { "log_disconnections", "off" }, \ + { "log_lock_waits", "on" }, \ + { "password_encryption", "md5" }, \ + { "ssl", "off" }, \ + { "ssl_ca_file", "" }, \ + { "ssl_crl_file", "" }, \ + { "ssl_cert_file", "" }, \ + { "ssl_key_file", "" }, \ + { "ssl_ciphers", "'" DEFAULT_SSL_CIPHERS "'" } #define DEFAULT_GUC_SETTINGS_FOR_PG_AUTO_FAILOVER_PRE_13 \ - DEFAULT_GUC_SETTINGS_FOR_PG_AUTO_FAILOVER, \ - { "wal_keep_segments", "512" } + DEFAULT_GUC_SETTINGS_FOR_PG_AUTO_FAILOVER, \ + { "wal_keep_segments", "512" } #define DEFAULT_GUC_SETTINGS_FOR_PG_AUTO_FAILOVER_13 \ - DEFAULT_GUC_SETTINGS_FOR_PG_AUTO_FAILOVER, \ - { "wal_keep_size", "'8 GB'" } + DEFAULT_GUC_SETTINGS_FOR_PG_AUTO_FAILOVER, \ + { "wal_keep_size", "'8 GB'" } GUC postgres_default_settings_pre_13[] = { DEFAULT_GUC_SETTINGS_FOR_PG_AUTO_FAILOVER_PRE_13, diff --git a/src/bin/pg_autoctl/service_monitor_init.c b/src/bin/pg_autoctl/service_monitor_init.c index 38de1aad2..583e09b29 100644 --- a/src/bin/pg_autoctl/service_monitor_init.c +++ b/src/bin/pg_autoctl/service_monitor_init.c @@ -135,7 +135,7 @@ service_monitor_init_start(void *context, pid_t *pid) /* finish the install if necessary */ if (!monitor_install(config->hostname, *pgSetup, false, - config->autoctl_node_password)) + config->autoctl_node_password)) { /* errors have already been logged */ exit(EXIT_CODE_INTERNAL_ERROR); diff --git a/src/bin/pg_autoctl/state.c b/src/bin/pg_autoctl/state.c index 553f3bef3..04cdb1d38 100644 --- a/src/bin/pg_autoctl/state.c +++ b/src/bin/pg_autoctl/state.c @@ -488,7 +488,9 @@ NodeStateToString(NodeState s) } default: + { return "Unknown State"; + } } } @@ -662,7 +664,9 @@ PreInitPostgreInstanceStateToString(PreInitPostgreInstanceState pgInitState) } default: + { return "unknown"; + } } /* keep compiler happy */ diff --git a/src/bin/pg_autoctl/systemd_config.c b/src/bin/pg_autoctl/systemd_config.c index 6e4de794b..87bd37432 100644 --- a/src/bin/pg_autoctl/systemd_config.c +++ b/src/bin/pg_autoctl/systemd_config.c @@ -24,57 +24,57 @@ #include "runprogram.h" #define OPTION_SYSTEMD_DESCRIPTION(config) \ - make_strbuf_option_default("Unit", "Description", NULL, true, BUFSIZE, \ - config->Description, "pg_auto_failover") + make_strbuf_option_default("Unit", "Description", NULL, true, BUFSIZE, \ + config->Description, "pg_auto_failover") #define OPTION_SYSTEMD_WORKING_DIRECTORY(config) \ - make_strbuf_option_default("Service", "WorkingDirectory", \ - NULL, true, BUFSIZE, \ - config->WorkingDirectory, "/var/lib/postgresql") + make_strbuf_option_default("Service", "WorkingDirectory", \ + NULL, true, BUFSIZE, \ + config->WorkingDirectory, "/var/lib/postgresql") #define OPTION_SYSTEMD_ENVIRONMENT_PGDATA(config) \ - make_strbuf_option_default("Service", "Environment", \ - NULL, true, BUFSIZE, \ - config->EnvironmentPGDATA, \ - "PGDATA=/var/lib/postgresql/11/pg_auto_failover") + make_strbuf_option_default("Service", "Environment", \ + NULL, true, BUFSIZE, \ + config->EnvironmentPGDATA, \ + "PGDATA=/var/lib/postgresql/11/pg_auto_failover") #define OPTION_SYSTEMD_USER(config) \ - make_strbuf_option_default("Service", "User", NULL, true, BUFSIZE, \ - config->User, "postgres") + make_strbuf_option_default("Service", "User", NULL, true, BUFSIZE, \ + config->User, "postgres") #define OPTION_SYSTEMD_EXECSTART(config) \ - make_strbuf_option_default("Service", "ExecStart", NULL, true, BUFSIZE, \ - config->ExecStart, "/usr/bin/pg_autoctl run") + make_strbuf_option_default("Service", "ExecStart", NULL, true, BUFSIZE, \ + config->ExecStart, "/usr/bin/pg_autoctl run") #define OPTION_SYSTEMD_RESTART(config) \ - make_strbuf_option_default("Service", "Restart", NULL, true, BUFSIZE, \ - config->Restart, "always") + make_strbuf_option_default("Service", "Restart", NULL, true, BUFSIZE, \ + config->Restart, "always") #define OPTION_SYSTEMD_STARTLIMITBURST(config) \ - make_int_option_default("Service", "StartLimitBurst", NULL, true, \ - &(config->StartLimitBurst), 20) + make_int_option_default("Service", "StartLimitBurst", NULL, true, \ + &(config->StartLimitBurst), 20) #define OPTION_SYSTEMD_EXECRELOAD(config) \ - make_strbuf_option_default("Service", "ExecReload", NULL, true, BUFSIZE, \ - config->ExecReload, "/usr/bin/pg_autoctl reload") + make_strbuf_option_default("Service", "ExecReload", NULL, true, BUFSIZE, \ + config->ExecReload, "/usr/bin/pg_autoctl reload") #define OPTION_SYSTEMD_WANTEDBY(config) \ - make_strbuf_option_default("Install", "WantedBy", NULL, true, BUFSIZE, \ - config->WantedBy, "multi-user.target") + make_strbuf_option_default("Install", "WantedBy", NULL, true, BUFSIZE, \ + config->WantedBy, "multi-user.target") #define SET_INI_OPTIONS_ARRAY(config) \ - { \ - OPTION_SYSTEMD_DESCRIPTION(config), \ - OPTION_SYSTEMD_WORKING_DIRECTORY(config), \ - OPTION_SYSTEMD_ENVIRONMENT_PGDATA(config), \ - OPTION_SYSTEMD_USER(config), \ - OPTION_SYSTEMD_EXECSTART(config), \ - OPTION_SYSTEMD_RESTART(config), \ - OPTION_SYSTEMD_STARTLIMITBURST(config), \ - OPTION_SYSTEMD_EXECRELOAD(config), \ - OPTION_SYSTEMD_WANTEDBY(config), \ - INI_OPTION_LAST \ - } + { \ + OPTION_SYSTEMD_DESCRIPTION(config), \ + OPTION_SYSTEMD_WORKING_DIRECTORY(config), \ + OPTION_SYSTEMD_ENVIRONMENT_PGDATA(config), \ + OPTION_SYSTEMD_USER(config), \ + OPTION_SYSTEMD_EXECSTART(config), \ + OPTION_SYSTEMD_RESTART(config), \ + OPTION_SYSTEMD_STARTLIMITBURST(config), \ + OPTION_SYSTEMD_EXECRELOAD(config), \ + OPTION_SYSTEMD_WANTEDBY(config), \ + INI_OPTION_LAST \ + } /* diff --git a/src/bin/pg_autoctl/watch.c b/src/bin/pg_autoctl/watch.c index eb29a6c81..c37c885d5 100644 --- a/src/bin/pg_autoctl/watch.c +++ b/src/bin/pg_autoctl/watch.c @@ -342,6 +342,7 @@ cli_watch_process_keys(WatchContext *context) } } } + /* left and right moves are conditionnal / relative */ else if (ch == KEY_LEFT || ch == ctrl('b') || ch == 'h') { @@ -365,6 +366,7 @@ cli_watch_process_keys(WatchContext *context) context->move = WATCH_MOVE_FOCUS_NONE; } } + /* left and right moves are conditionnal / relative */ else if (ch == KEY_RIGHT || ch == ctrl('f') || ch == 'l') { @@ -380,6 +382,7 @@ cli_watch_process_keys(WatchContext *context) context->move = WATCH_MOVE_FOCUS_NONE; } } + /* home and end moves are unconditionnal / absolute */ else if (ch == KEY_HOME || ch == ctrl('a') || ch == '0') { @@ -391,6 +394,7 @@ cli_watch_process_keys(WatchContext *context) { context->move = WATCH_MOVE_FOCUS_END; } + /* up is C-p in Emacs, k in vi(m) */ else if (ch == KEY_UP || ch == ctrl('p') || ch == 'k') { @@ -401,6 +405,7 @@ cli_watch_process_keys(WatchContext *context) --context->selectedRow; } } + /* page up, which is also C-u in the terminal with less/more etc */ else if (ch == KEY_PPAGE || ch == ctrl('u')) { @@ -413,6 +418,7 @@ cli_watch_process_keys(WatchContext *context) context->selectedRow -= 5; } } + /* down is C-n in Emacs, j in vi(m) */ else if (ch == KEY_DOWN || ch == ctrl('n') || ch == 'j') { @@ -423,6 +429,7 @@ cli_watch_process_keys(WatchContext *context) ++context->selectedRow; } } + /* page down, which is also C-d in the terminal with less/more etc */ else if (ch == KEY_NPAGE || ch == ctrl('d')) { @@ -436,6 +443,7 @@ cli_watch_process_keys(WatchContext *context) context->selectedRow += 5; } } + /* cancel current selected row */ else if (ch == KEY_DL || ch == KEY_DC) { @@ -561,8 +569,8 @@ cli_watch_render(WatchContext *context, WatchContext *previous) context->startCol != previous->startCol || context->cookedMode != previous->cookedMode || context->eventsArray.count != previous->eventsArray.count || - (context->eventsArray.events[0].eventId != - previous->eventsArray.events[0].eventId)) + (context->eventsArray.events[0].eventId != previous->eventsArray.events[0].eventId + )) { (void) clear_line_at(++printedRows); diff --git a/src/bin/pgaftest/compose_gen.c b/src/bin/pgaftest/compose_gen.c index e37a5b4b4..94524f5f4 100644 --- a/src/bin/pgaftest/compose_gen.c +++ b/src/bin/pgaftest/compose_gen.c @@ -48,7 +48,7 @@ * The monitor hostname is always "monitor" inside the compose network. */ #define MONITOR_PGURI \ - "postgresql://autoctl_node@monitor/pg_auto_failover" + "postgresql://autoctl_node@monitor/pg_auto_failover" /* Fixed path inside every container */ #define NODE_INI_PATH "/etc/pgaf/node.ini" @@ -81,21 +81,21 @@ debian_pg_version(void) * - CA cert copied to ~/.postgresql/root.crt so libpq trusts server certs. */ #define SSL_COPY_CERTS_CMD \ - "cp " SSL_DIR_IN_CONTAINER "/server/server.crt /var/lib/postgres/server.crt" \ - " && cp " SSL_DIR_IN_CONTAINER \ - "/server/server.key /var/lib/postgres/server.key" \ - " && chmod 0600 /var/lib/postgres/server.key" \ - " && mkdir -p /var/lib/postgres/.postgresql" \ - " && cp " \ - SSL_DIR_IN_CONTAINER \ - "/client/postgresql.crt /var/lib/postgres/.postgresql/postgresql.crt" \ - " && cp " \ - SSL_DIR_IN_CONTAINER \ - "/client/postgresql.key /var/lib/postgres/.postgresql/postgresql.key" \ - " && chmod 0600 /var/lib/postgres/.postgresql/postgresql.key" \ - " && cp " \ - SSL_DIR_IN_CONTAINER "/ca.crt /var/lib/postgres/.postgresql/root.crt" \ - " &&" + "cp " SSL_DIR_IN_CONTAINER "/server/server.crt /var/lib/postgres/server.crt" \ + " && cp " SSL_DIR_IN_CONTAINER \ + "/server/server.key /var/lib/postgres/server.key" \ + " && chmod 0600 /var/lib/postgres/server.key" \ + " && mkdir -p /var/lib/postgres/.postgresql" \ + " && cp " \ + SSL_DIR_IN_CONTAINER \ + "/client/postgresql.crt /var/lib/postgres/.postgresql/postgresql.crt" \ + " && cp " \ + SSL_DIR_IN_CONTAINER \ + "/client/postgresql.key /var/lib/postgres/.postgresql/postgresql.key" \ + " && chmod 0600 /var/lib/postgres/.postgresql/postgresql.key" \ + " && cp " \ + SSL_DIR_IN_CONTAINER "/ca.crt /var/lib/postgres/.postgresql/root.crt" \ + " &&" /* @@ -520,9 +520,9 @@ compose_gen_write(TestCluster *cluster, { fformat(f, " - ./ssl/ca.crt:" SSL_DIR_IN_CONTAINER "/ca.crt:ro\n" - " - ./ssl/monitor:" + " - ./ssl/monitor:" SSL_DIR_IN_CONTAINER "/server:ro\n" - " - ./ssl/client:" + " - ./ssl/client:" SSL_DIR_IN_CONTAINER "/client:ro\n"); } if (cluster->bindSource) @@ -549,7 +549,7 @@ compose_gen_write(TestCluster *cluster, " command: [\"/bin/sh\", \"-c\"," " \"%s rm -f /tmp/pg_autoctl%s/pg_autoctl.pid" " && exec pg_autoctl node run " NODE_INI_PATH "\"]\n" - " stop_grace_period: 60s\n\n", + " stop_grace_period: 60s\n\n", ssl_needs_certs(cluster->ssl) ? SSL_COPY_CERTS_CMD : "", monitor_pgdata); } @@ -575,9 +575,9 @@ compose_gen_write(TestCluster *cluster, { fformat(f, " - ./ssl/ca.crt:" SSL_DIR_IN_CONTAINER "/ca.crt:ro\n" - " - ./ssl/%s:" + " - ./ssl/%s:" SSL_DIR_IN_CONTAINER "/server:ro\n" - " - ./ssl/client:" + " - ./ssl/client:" SSL_DIR_IN_CONTAINER "/client:ro\n", svc); } @@ -596,9 +596,9 @@ compose_gen_write(TestCluster *cluster, fformat(f, " command: [\"/bin/sh\", \"-c\"," " \"%s rm -f /tmp/pg_autoctl" NODE_PGDATA "/pg_autoctl.pid" - " && exec pg_autoctl node run " + " && exec pg_autoctl node run " NODE_INI_PATH "\"]\n" - " stop_grace_period: 60s\n\n", + " stop_grace_period: 60s\n\n", ssl_needs_certs(cluster->ssl) ? SSL_COPY_CERTS_CMD : ""); } @@ -655,9 +655,9 @@ compose_gen_write(TestCluster *cluster, { fformat(f, " - ./ssl/ca.crt:" SSL_DIR_IN_CONTAINER "/ca.crt:ro\n" - " - ./ssl/%s:" + " - ./ssl/%s:" SSL_DIR_IN_CONTAINER "/server:ro\n" - " - ./ssl/client:" + " - ./ssl/client:" SSL_DIR_IN_CONTAINER "/client:ro\n", n->name); } @@ -688,7 +688,7 @@ compose_gen_write(TestCluster *cluster, " command: [\"/bin/sh\", \"-c\"," " \"%s rm -f /tmp/pg_autoctl%s/pg_autoctl.pid" " && exec pg_autoctl node run " NODE_INI_PATH "\"]\n" - " stop_grace_period: 60s\n", + " stop_grace_period: 60s\n", ssl_needs_certs(node_ssl) ? SSL_COPY_CERTS_CMD : "", node_pgdata); @@ -910,8 +910,8 @@ compose_gen_write_monitor_ini(const TestCluster *cluster, const char *dir) * user) in the start command so the key is owned by the container * user and PostgreSQL can open it. */ - "cert_file = /var/lib/postgres/server.crt\n" - "key_file = /var/lib/postgres/server.key\n"); + "cert_file = /var/lib/postgres/server.crt\n" + "key_file = /var/lib/postgres/server.key\n"); } if (cluster->monitorPassword[0]) @@ -978,11 +978,11 @@ compose_gen_write_second_monitor_ini(const TestCluster *cluster, const char *dir "\n" "[postgresql]\n" "pgdata = " NODE_PGDATA "\n" - "\n" - "[options]\n" - "ssl = %s\n" - "auth = %s\n" - "pg_hba_lan = false\n", + "\n" + "[options]\n" + "ssl = %s\n" + "auth = %s\n" + "pg_hba_lan = false\n", name, name, cluster->ssl, cluster->auth); @@ -1000,8 +1000,8 @@ compose_gen_write_second_monitor_ini(const TestCluster *cluster, const char *dir * user) in the start command so the key is owned by the container * user and PostgreSQL can open it. */ - "cert_file = /var/lib/postgres/server.crt\n" - "key_file = /var/lib/postgres/server.key\n"); + "cert_file = /var/lib/postgres/server.crt\n" + "key_file = /var/lib/postgres/server.key\n"); } fclose(f); @@ -1085,7 +1085,7 @@ compose_gen_write_node_ini(const TestCluster *cluster, "\n" "[postgresql]\n" "pgdata = " DEBIAN_PGDATA_PREFIX "/%s/%s\n" - "\n", + "\n", debian_pg_version(), node->debianCluster); } else @@ -1094,7 +1094,7 @@ compose_gen_write_node_ini(const TestCluster *cluster, "\n" "[postgresql]\n" "pgdata = " NODE_PGDATA "\n" - "\n"); + "\n"); } if (node->noMonitor) @@ -1119,7 +1119,7 @@ compose_gen_write_node_ini(const TestCluster *cluster, fformat(f, "[monitor]\n" "pguri = " MONITOR_PGURI "\n" - "\n"); + "\n"); } fformat(f, @@ -1172,8 +1172,8 @@ compose_gen_write_node_ini(const TestCluster *cluster, * user) in the start command so the key is owned by the container * user and PostgreSQL can open it. */ - "cert_file = /var/lib/postgres/server.crt\n" - "key_file = /var/lib/postgres/server.key\n"); + "cert_file = /var/lib/postgres/server.crt\n" + "key_file = /var/lib/postgres/server.key\n"); } if (node->listen) diff --git a/src/bin/pgaftest/test_runner.c b/src/bin/pgaftest/test_runner.c index c0f528c34..5803dcf44 100644 --- a/src/bin/pgaftest/test_runner.c +++ b/src/bin/pgaftest/test_runner.c @@ -1219,7 +1219,7 @@ wait_for_states(TestRunner *r, TestCmd *cmd) time_t t0 = time(NULL); if (!monitor_wait_formation_states(r, - (const char (*)[64])cmd->waitStates, + (const char (*)[64]) cmd->waitStates, cmd->waitStateCount, cmd->waitGroups, cmd->waitGroupCount, @@ -2176,7 +2176,7 @@ runner_exec_cmd(TestRunner *r, TestCmd *cmd, char *errBuf, int errLen) bool seenThrough[PGAF_MAX_WAIT_STATES] = { false }; if (!runner_wait_notify_goal(r, cmd->service, cmd->state, - (const char (*)[64])cmd->passThroughStates, + (const char (*)[64]) cmd->passThroughStates, seenThrough, cmd->passThroughCount, cmd->timeoutSeconds)) { diff --git a/src/bin/pgaftest/test_spec_parse.c b/src/bin/pgaftest/test_spec_parse.c index 2fa5e85fa..bc41648e1 100644 --- a/src/bin/pgaftest/test_spec_parse.c +++ b/src/bin/pgaftest/test_spec_parse.c @@ -326,7 +326,7 @@ static void yyerror(const char *msg) { fprintf(/* IGNORE-BANNED */ stderr, "pgaftest: parse error at line %d: %s\n", - pgaf_line_number, msg); + pgaf_line_number, msg); exit(1); } @@ -703,24 +703,24 @@ union yyalloc /* The size of an array large to enough to hold all stacks, each with * N elements. */ # define YYSTACK_BYTES(N) \ - ((N) *(sizeof(yytype_int16) + sizeof(YYSTYPE)) \ - + YYSTACK_GAP_MAXIMUM) + ((N) *(sizeof(yytype_int16) + sizeof(YYSTYPE)) \ + + YYSTACK_GAP_MAXIMUM) /* Copy COUNT objects from FROM to TO. The source and destination do * not overlap. */ # ifndef YYCOPY # if defined __GNUC__ && 1 < __GNUC__ # define YYCOPY(To, From, Count) \ - __builtin_memcpy(To, From, (Count) * sizeof(*(From))) + __builtin_memcpy(To, From, (Count) * sizeof(*(From))) # else # define YYCOPY(To, From, Count) \ - do \ - { \ - YYSIZE_T yyi; \ - for (yyi = 0; yyi < (Count); yyi++) { \ - (To)[yyi] = (From)[yyi]; } \ - } \ - while (YYID(0)) + do \ + { \ + YYSIZE_T yyi; \ + for (yyi = 0; yyi < (Count); yyi++) { \ + (To)[yyi] = (From)[yyi]; } \ + } \ + while (YYID(0)) # endif # endif @@ -730,15 +730,15 @@ union yyalloc * stack. Advance YYPTR to a properly aligned location for the next * stack. */ # define YYSTACK_RELOCATE(Stack) \ - do \ - { \ - YYSIZE_T yynewbytes; \ - YYCOPY(&yyptr->Stack, Stack, yysize); \ - Stack = &yyptr->Stack; \ - yynewbytes = yystacksize * sizeof(*Stack) + YYSTACK_GAP_MAXIMUM; \ - yyptr += yynewbytes / sizeof(*yyptr); \ - } \ - while (YYID(0)) + do \ + { \ + YYSIZE_T yynewbytes; \ + YYCOPY(&yyptr->Stack, Stack, yysize); \ + Stack = &yyptr->Stack; \ + yynewbytes = yystacksize * sizeof(*Stack) + YYSTACK_GAP_MAXIMUM; \ + yyptr += yynewbytes / sizeof(*yyptr); \ + } \ + while (YYID(0)) #endif @@ -765,7 +765,7 @@ union yyalloc #define YYMAXUTOK 362 #define YYTRANSLATE(YYX) \ - ((unsigned int) (YYX) <= YYMAXUTOK ? yytranslate[YYX] : YYUNDEFTOK) + ((unsigned int) (YYX) <= YYMAXUTOK ? yytranslate[YYX] : YYUNDEFTOK) /* YYTRANSLATE[YYLEX] -- Bison symbol number corresponding to YYLEX. */ static const yytype_uint8 yytranslate[] = @@ -1322,7 +1322,7 @@ static const yytype_uint8 yystos[] = #define YYRECOVERING() (!!yyerrstatus) #define YYBACKUP(Token, Value) \ - do \ + do \ if (yychar == YYEMPTY && yylen == 1) \ { \ yychar = (Token); \ @@ -1336,7 +1336,7 @@ static const yytype_uint8 yystos[] = yyerror(YY_("syntax error: cannot back up")); \ YYERROR; \ } \ - while (YYID(0)) + while (YYID(0)) #define YYTERROR 1 @@ -1350,7 +1350,7 @@ static const yytype_uint8 yystos[] = #define YYRHSLOC(Rhs, K) ((Rhs)[K]) #ifndef YYLLOC_DEFAULT # define YYLLOC_DEFAULT(Current, Rhs, N) \ - do \ + do \ if (YYID(N)) \ { \ (Current).first_line = YYRHSLOC(Rhs, 1).first_line; \ @@ -1365,7 +1365,7 @@ static const yytype_uint8 yystos[] = (Current).first_column = (Current).last_column = \ YYRHSLOC(Rhs, 0).last_column; \ } \ - while (YYID(0)) + while (YYID(0)) #endif @@ -1376,9 +1376,9 @@ static const yytype_uint8 yystos[] = #ifndef YY_LOCATION_PRINT # if defined YYLTYPE_IS_TRIVIAL && YYLTYPE_IS_TRIVIAL # define YY_LOCATION_PRINT(File, Loc) \ - fprintf(/* IGNORE-BANNED */ File, "%d.%d-%d.%d", \ - (Loc).first_line, (Loc).first_column, \ - (Loc).last_line, (Loc).last_column) + fprintf(/* IGNORE-BANNED */ File, "%d.%d-%d.%d", \ + (Loc).first_line, (Loc).first_column, \ + (Loc).last_line, (Loc).last_column) # else # define YY_LOCATION_PRINT(File, Loc) ((void) 0) # endif @@ -1402,21 +1402,21 @@ static const yytype_uint8 yystos[] = # endif # define YYDPRINTF(Args) \ - do { \ - if (yydebug) { \ - YYFPRINTF Args; } \ - } while (YYID(0)) + do { \ + if (yydebug) { \ + YYFPRINTF Args; } \ + } while (YYID(0)) # define YY_SYMBOL_PRINT(Title, Type, Value, Location) \ - do { \ - if (yydebug) \ - { \ - YYFPRINTF(stderr, "%s ", Title); \ - yy_symbol_print(stderr, \ - Type, Value); \ - YYFPRINTF(stderr, "\n"); \ - } \ - } while (YYID(0)) + do { \ + if (yydebug) \ + { \ + YYFPRINTF(stderr, "%s ", Title); \ + yy_symbol_print(stderr, \ + Type, Value); \ + YYFPRINTF(stderr, "\n"); \ + } \ + } while (YYID(0)) /*--------------------------------. @@ -1507,10 +1507,10 @@ yytype_int16 *top; } # define YY_STACK_PRINT(Bottom, Top) \ - do { \ - if (yydebug) { \ - yy_stack_print((Bottom), (Top)); } \ - } while (YYID(0)) + do { \ + if (yydebug) { \ + yy_stack_print((Bottom), (Top)); } \ + } while (YYID(0)) /*------------------------------------------------. @@ -1544,10 +1544,10 @@ int yyrule; } # define YY_REDUCE_PRINT(Rule) \ - do { \ - if (yydebug) { \ - yy_reduce_print(yyvsp, Rule); } \ - } while (YYID(0)) + do { \ + if (yydebug) { \ + yy_reduce_print(yyvsp, Rule); } \ + } while (YYID(0)) /* Nonzero means print parse trace. It is left uninitialized so that * multiple parsers can coexist. */ @@ -1680,11 +1680,13 @@ yytnamerr(char *yyres, const char *yystr) } case '"': + { if (yyres) { yyres[yyn] = '\0'; } return yyn; + } } } do_not_strip_quotes:; @@ -2181,1570 +2183,1570 @@ yyparse() { case 9: #line 233 "test_spec_parse.y" - { - strlcpy(current_spec->cluster.ssl, "self-signed", - sizeof(current_spec->cluster.ssl)); - strlcpy(current_spec->cluster.auth, "trust", - sizeof(current_spec->cluster.auth)); - } + { + strlcpy(current_spec->cluster.ssl, "self-signed", + sizeof(current_spec->cluster.ssl)); + strlcpy(current_spec->cluster.auth, "trust", + sizeof(current_spec->cluster.auth)); break; + } case 19: #line 254 "test_spec_parse.y" - { - current_spec->cluster.bindSource = true; - } + { + current_spec->cluster.bindSource = true; break; + } case 20: #line 268 "test_spec_parse.y" - { - current_spec->cluster.withMonitor = true; - } + { + current_spec->cluster.withMonitor = true; break; + } case 21: #line 272 "test_spec_parse.y" - { - current_spec->cluster.withMonitor = true; - strlcpy(current_spec->cluster.monitorDebianCluster, (yyvsp[(3) - - (3)].str), - sizeof(current_spec->cluster.monitorDebianCluster)); - free((yyvsp[(3) - (3)].str)); - } + { + current_spec->cluster.withMonitor = true; + strlcpy(current_spec->cluster.monitorDebianCluster, (yyvsp[(3) - + (3)].str), + sizeof(current_spec->cluster.monitorDebianCluster)); + free((yyvsp[(3) - (3)].str)); break; + } case 22: #line 279 "test_spec_parse.y" - { - current_spec->cluster.withMonitor = true; - strlcpy(current_spec->cluster.monitorImageTarget, (yyvsp[(3) - (3)].str), - sizeof(current_spec->cluster.monitorImageTarget)); - free((yyvsp[(3) - (3)].str)); - } + { + current_spec->cluster.withMonitor = true; + strlcpy(current_spec->cluster.monitorImageTarget, (yyvsp[(3) - (3)].str), + sizeof(current_spec->cluster.monitorImageTarget)); + free((yyvsp[(3) - (3)].str)); break; + } case 23: #line 286 "test_spec_parse.y" - { - current_spec->cluster.withMonitor = true; + { + current_spec->cluster.withMonitor = true; - /* monitor port not stored in TestCluster yet; ignore */ - (void) (yyvsp[(3) - (3)].ival); - } + /* monitor port not stored in TestCluster yet; ignore */ + (void) (yyvsp[(3) - (3)].ival); break; + } case 24: #line 292 "test_spec_parse.y" - { - current_spec->cluster.withMonitor = true; - strlcpy(current_spec->cluster.monitorPassword, (yyvsp[(3) - (3)].str), - sizeof(current_spec->cluster.monitorPassword)); - free((yyvsp[(3) - (3)].str)); - } + { + current_spec->cluster.withMonitor = true; + strlcpy(current_spec->cluster.monitorPassword, (yyvsp[(3) - (3)].str), + sizeof(current_spec->cluster.monitorPassword)); + free((yyvsp[(3) - (3)].str)); break; + } case 25: #line 299 "test_spec_parse.y" - { - strlcpy(current_spec->cluster.secondMonitorName, (yyvsp[(2) - (4)].str), - sizeof(current_spec->cluster.secondMonitorName)); - current_spec->cluster.secondMonitorStopped = true; - free((yyvsp[(2) - (4)].str)); - } + { + strlcpy(current_spec->cluster.secondMonitorName, (yyvsp[(2) - (4)].str), + sizeof(current_spec->cluster.secondMonitorName)); + current_spec->cluster.secondMonitorStopped = true; + free((yyvsp[(2) - (4)].str)); break; + } case 26: #line 306 "test_spec_parse.y" - { - strlcpy(current_spec->cluster.secondMonitorName, (yyvsp[(2) - (4)].str), - sizeof(current_spec->cluster.secondMonitorName)); - current_spec->cluster.secondMonitorStopped = true; - free((yyvsp[(2) - (4)].str)); - } + { + strlcpy(current_spec->cluster.secondMonitorName, (yyvsp[(2) - (4)].str), + sizeof(current_spec->cluster.secondMonitorName)); + current_spec->cluster.secondMonitorStopped = true; + free((yyvsp[(2) - (4)].str)); break; + } case 27: #line 313 "test_spec_parse.y" - { - strlcpy(current_spec->cluster.secondMonitorName, (yyvsp[(2) - (6)].str), - sizeof(current_spec->cluster.secondMonitorName)); - current_spec->cluster.secondMonitorStopped = true; - free((yyvsp[(2) - (6)].str)); + { + strlcpy(current_spec->cluster.secondMonitorName, (yyvsp[(2) - (6)].str), + sizeof(current_spec->cluster.secondMonitorName)); + current_spec->cluster.secondMonitorStopped = true; + free((yyvsp[(2) - (6)].str)); - /* password for second monitor not yet stored */ - free((yyvsp[(6) - (6)].str)); - } + /* password for second monitor not yet stored */ + free((yyvsp[(6) - (6)].str)); break; + } case 28: #line 326 "test_spec_parse.y" - { - strlcpy(current_spec->cluster.image, (yyvsp[(2) - (2)].str), - sizeof(current_spec->cluster.image)); - free((yyvsp[(2) - (2)].str)); - } + { + strlcpy(current_spec->cluster.image, (yyvsp[(2) - (2)].str), + sizeof(current_spec->cluster.image)); + free((yyvsp[(2) - (2)].str)); break; + } case 29: #line 332 "test_spec_parse.y" - { - strlcpy(current_spec->cluster.image, (yyvsp[(2) - (2)].str), - sizeof(current_spec->cluster.image)); - free((yyvsp[(2) - (2)].str)); - } + { + strlcpy(current_spec->cluster.image, (yyvsp[(2) - (2)].str), + sizeof(current_spec->cluster.image)); + free((yyvsp[(2) - (2)].str)); break; + } case 30: #line 342 "test_spec_parse.y" - { - strlcpy(current_spec->cluster.extensionVersion, (yyvsp[(2) - (2)].str), - sizeof(current_spec->cluster.extensionVersion)); - free((yyvsp[(2) - (2)].str)); - } + { + strlcpy(current_spec->cluster.extensionVersion, (yyvsp[(2) - (2)].str), + sizeof(current_spec->cluster.extensionVersion)); + free((yyvsp[(2) - (2)].str)); break; + } case 31: #line 348 "test_spec_parse.y" - { - strlcpy(current_spec->cluster.extensionVersion, (yyvsp[(2) - (2)].str), - sizeof(current_spec->cluster.extensionVersion)); - free((yyvsp[(2) - (2)].str)); - } + { + strlcpy(current_spec->cluster.extensionVersion, (yyvsp[(2) - (2)].str), + sizeof(current_spec->cluster.extensionVersion)); + free((yyvsp[(2) - (2)].str)); break; + } case 32: #line 358 "test_spec_parse.y" - { - strlcpy(current_spec->cluster.ssl, (yyvsp[(2) - (2)].str), - sizeof(current_spec->cluster.ssl)); - free((yyvsp[(2) - (2)].str)); - } + { + strlcpy(current_spec->cluster.ssl, (yyvsp[(2) - (2)].str), + sizeof(current_spec->cluster.ssl)); + free((yyvsp[(2) - (2)].str)); break; + } case 33: #line 368 "test_spec_parse.y" - { - strlcpy(current_spec->cluster.auth, (yyvsp[(2) - (2)].str), - sizeof(current_spec->cluster.auth)); - free((yyvsp[(2) - (2)].str)); - } + { + strlcpy(current_spec->cluster.auth, (yyvsp[(2) - (2)].str), + sizeof(current_spec->cluster.auth)); + free((yyvsp[(2) - (2)].str)); break; + } case 34: #line 374 "test_spec_parse.y" - { - strlcpy(current_spec->cluster.auth, (yyvsp[(2) - (2)].str), - sizeof(current_spec->cluster.auth)); - free((yyvsp[(2) - (2)].str)); - } + { + strlcpy(current_spec->cluster.auth, (yyvsp[(2) - (2)].str), + sizeof(current_spec->cluster.auth)); + free((yyvsp[(2) - (2)].str)); break; + } case 35: #line 384 "test_spec_parse.y" + { + TestCluster *cl = ¤t_spec->cluster; + if (cl->formationCount >= PGAF_MAX_FORMATIONS) { - TestCluster *cl = ¤t_spec->cluster; - if (cl->formationCount >= PGAF_MAX_FORMATIONS) - { - fprintf(/* IGNORE-BANNED */ stderr, - "pgaftest: too many formations (max %d)\n", - PGAF_MAX_FORMATIONS); - exit(1); - } - current_formation = &cl->formations[cl->formationCount++]; - strlcpy(current_formation->name, "default", - sizeof(current_formation->name)); - current_formation->numSync = -1; + fprintf(/* IGNORE-BANNED */ stderr, + "pgaftest: too many formations (max %d)\n", + PGAF_MAX_FORMATIONS); + exit(1); } + current_formation = &cl->formations[cl->formationCount++]; + strlcpy(current_formation->name, "default", + sizeof(current_formation->name)); + current_formation->numSync = -1; break; + } case 39: #line 411 "test_spec_parse.y" - { - (yyval.str) = (yyvsp[(1) - (1)].str); - } + { + (yyval.str) = (yyvsp[(1) - (1)].str); break; + } case 40: #line 412 "test_spec_parse.y" - { - (yyval.str) = (yyvsp[(1) - (1)].str); - } + { + (yyval.str) = (yyvsp[(1) - (1)].str); break; + } case 41: #line 413 "test_spec_parse.y" - { - (yyval.str) = strdup("auth"); - } + { + (yyval.str) = strdup("auth"); break; + } case 42: #line 414 "test_spec_parse.y" - { - (yyval.str) = strdup("monitor"); - } + { + (yyval.str) = strdup("monitor"); break; + } case 43: #line 415 "test_spec_parse.y" - { - (yyval.str) = strdup("node"); - } + { + (yyval.str) = strdup("node"); break; + } case 44: #line 420 "test_spec_parse.y" - { - strlcpy(current_formation->name, (yyvsp[(1) - (1)].str), - sizeof(current_formation->name)); - free((yyvsp[(1) - (1)].str)); - } + { + strlcpy(current_formation->name, (yyvsp[(1) - (1)].str), + sizeof(current_formation->name)); + free((yyvsp[(1) - (1)].str)); break; + } case 45: #line 425 "test_spec_parse.y" - { - current_formation->numSync = (yyvsp[(2) - (2)].ival); - } + { + current_formation->numSync = (yyvsp[(2) - (2)].ival); break; + } case 48: #line 451 "test_spec_parse.y" - { - (yyval.str) = (yyvsp[(1) - (1)].str); - } + { + (yyval.str) = (yyvsp[(1) - (1)].str); break; + } case 49: #line 452 "test_spec_parse.y" - { - (yyval.str) = strdup("monitor"); - } + { + (yyval.str) = strdup("monitor"); break; + } case 50: #line 461 "test_spec_parse.y" + { + if (current_formation->nodeCount >= PGAF_MAX_NODES) { - if (current_formation->nodeCount >= PGAF_MAX_NODES) - { - fprintf(/* IGNORE-BANNED */ stderr, - "pgaftest: too many nodes in formation (max %d)\n", - PGAF_MAX_NODES); - exit(1); - } - current_node = ¤t_formation->nodes[current_formation->nodeCount++]; - current_node->kind = NODE_KIND_STANDALONE; - current_node->candidatePriority = 50; - current_node->replicationQuorum = true; + fprintf(/* IGNORE-BANNED */ stderr, + "pgaftest: too many nodes in formation (max %d)\n", + PGAF_MAX_NODES); + exit(1); } + current_node = ¤t_formation->nodes[current_formation->nodeCount++]; + current_node->kind = NODE_KIND_STANDALONE; + current_node->candidatePriority = 50; + current_node->replicationQuorum = true; break; + } case 51: #line 478 "test_spec_parse.y" - { - strlcpy(current_node->name, (yyvsp[(1) - (2)].str), - sizeof(current_node->name)); - free((yyvsp[(1) - (2)].str)); - } + { + strlcpy(current_node->name, (yyvsp[(1) - (2)].str), + sizeof(current_node->name)); + free((yyvsp[(1) - (2)].str)); break; + } case 53: #line 485 "test_spec_parse.y" - { - strlcpy(current_node->name, (yyvsp[(2) - (3)].str), - sizeof(current_node->name)); - free((yyvsp[(2) - (3)].str)); - } + { + strlcpy(current_node->name, (yyvsp[(2) - (3)].str), + sizeof(current_node->name)); + free((yyvsp[(2) - (3)].str)); break; + } case 57: #line 499 "test_spec_parse.y" - { - current_node->kind = NODE_KIND_CITUS_COORDINATOR; - current_spec->cluster.withCitus = true; - } + { + current_node->kind = NODE_KIND_CITUS_COORDINATOR; + current_spec->cluster.withCitus = true; break; + } case 58: #line 504 "test_spec_parse.y" - { - current_node->kind = NODE_KIND_CITUS_WORKER; - current_spec->cluster.withCitus = true; - } + { + current_node->kind = NODE_KIND_CITUS_WORKER; + current_spec->cluster.withCitus = true; break; + } case 59: #line 509 "test_spec_parse.y" - { - current_node->replicationQuorum = false; - } + { + current_node->replicationQuorum = false; break; + } case 60: #line 513 "test_spec_parse.y" - { - current_node->noMonitor = true; - } + { + current_node->noMonitor = true; break; + } case 61: #line 517 "test_spec_parse.y" - { - current_node->launchDeferred = true; - } + { + current_node->launchDeferred = true; break; + } case 62: #line 521 "test_spec_parse.y" - { - current_node->launchDeferred = true; - } + { + current_node->launchDeferred = true; break; + } case 63: #line 525 "test_spec_parse.y" - { - current_node->launchDeferred = false; - } + { + current_node->launchDeferred = false; break; + } case 64: #line 529 "test_spec_parse.y" - { - current_node->launchDeferred = false; - } + { + current_node->launchDeferred = false; break; + } case 65: #line 533 "test_spec_parse.y" - { - current_node->listen = true; - } + { + current_node->listen = true; break; + } case 66: #line 537 "test_spec_parse.y" - { - current_node->citusSecondary = true; - } + { + current_node->citusSecondary = true; break; + } case 67: #line 541 "test_spec_parse.y" - { - current_node->candidatePriority = (yyvsp[(2) - (2)].ival); - } + { + current_node->candidatePriority = (yyvsp[(2) - (2)].ival); break; + } case 68: #line 545 "test_spec_parse.y" - { - current_node->group = (yyvsp[(2) - (2)].ival); - } + { + current_node->group = (yyvsp[(2) - (2)].ival); break; + } case 69: #line 549 "test_spec_parse.y" - { - current_node->pgPort = (yyvsp[(2) - (2)].ival); - } + { + current_node->pgPort = (yyvsp[(2) - (2)].ival); break; + } case 70: #line 553 "test_spec_parse.y" - { - strlcpy(current_node->citusClusterName, (yyvsp[(2) - (2)].str), - sizeof(current_node->citusClusterName)); - free((yyvsp[(2) - (2)].str)); - } + { + strlcpy(current_node->citusClusterName, (yyvsp[(2) - (2)].str), + sizeof(current_node->citusClusterName)); + free((yyvsp[(2) - (2)].str)); break; + } case 71: #line 559 "test_spec_parse.y" - { - strlcpy(current_node->debianCluster, (yyvsp[(2) - (2)].str), - sizeof(current_node->debianCluster)); - free((yyvsp[(2) - (2)].str)); - } + { + strlcpy(current_node->debianCluster, (yyvsp[(2) - (2)].str), + sizeof(current_node->debianCluster)); + free((yyvsp[(2) - (2)].str)); break; + } case 72: #line 565 "test_spec_parse.y" - { - strlcpy(current_node->ssl, (yyvsp[(2) - (2)].str), - sizeof(current_node->ssl)); - free((yyvsp[(2) - (2)].str)); - } + { + strlcpy(current_node->ssl, (yyvsp[(2) - (2)].str), + sizeof(current_node->ssl)); + free((yyvsp[(2) - (2)].str)); break; + } case 73: #line 570 "test_spec_parse.y" - { - strlcpy(current_node->auth, (yyvsp[(2) - (2)].str), - sizeof(current_node->auth)); - free((yyvsp[(2) - (2)].str)); - } + { + strlcpy(current_node->auth, (yyvsp[(2) - (2)].str), + sizeof(current_node->auth)); + free((yyvsp[(2) - (2)].str)); break; + } case 74: #line 575 "test_spec_parse.y" - { - strlcpy(current_node->auth, (yyvsp[(2) - (2)].str), - sizeof(current_node->auth)); - free((yyvsp[(2) - (2)].str)); - } + { + strlcpy(current_node->auth, (yyvsp[(2) - (2)].str), + sizeof(current_node->auth)); + free((yyvsp[(2) - (2)].str)); break; + } case 75: #line 580 "test_spec_parse.y" + { + if (strcmp((yyvsp[(2) - (2)].str), "false") == 0 || strcmp((yyvsp[(2) - + (2)].str), + "0") == 0) { - if (strcmp((yyvsp[(2) - (2)].str), "false") == 0 || strcmp((yyvsp[(2) - - (2)].str), - "0") == 0) - { - current_node->replicationQuorum = false; - } - else - { - current_node->replicationQuorum = true; - } - free((yyvsp[(2) - (2)].str)); + current_node->replicationQuorum = false; + } + else + { + current_node->replicationQuorum = true; } + free((yyvsp[(2) - (2)].str)); break; + } case 76: #line 588 "test_spec_parse.y" - { - strlcpy(current_node->replicationPassword, (yyvsp[(2) - (2)].str), - sizeof(current_node->replicationPassword)); - free((yyvsp[(2) - (2)].str)); - } + { + strlcpy(current_node->replicationPassword, (yyvsp[(2) - (2)].str), + sizeof(current_node->replicationPassword)); + free((yyvsp[(2) - (2)].str)); break; + } case 77: #line 594 "test_spec_parse.y" - { - strlcpy(current_node->monitorPassword, (yyvsp[(2) - (2)].str), - sizeof(current_node->monitorPassword)); - free((yyvsp[(2) - (2)].str)); - } + { + strlcpy(current_node->monitorPassword, (yyvsp[(2) - (2)].str), + sizeof(current_node->monitorPassword)); + free((yyvsp[(2) - (2)].str)); break; + } case 78: #line 600 "test_spec_parse.y" + { + /* volume — adds a named Docker volume */ + int vi = current_node->volumeCount; + if (vi < PGAF_MAX_NODE_VOLUMES) { - /* volume — adds a named Docker volume */ - int vi = current_node->volumeCount; - if (vi < PGAF_MAX_NODE_VOLUMES) - { - strlcpy(current_node->volumes[vi].name, (yyvsp[(2) - (3)].str), - sizeof(current_node->volumes[0].name)); - strlcpy(current_node->volumes[vi].path, (yyvsp[(3) - (3)].str), - sizeof(current_node->volumes[0].path)); - current_node->volumeCount++; - } - free((yyvsp[(2) - (3)].str)); - free((yyvsp[(3) - (3)].str)); + strlcpy(current_node->volumes[vi].name, (yyvsp[(2) - (3)].str), + sizeof(current_node->volumes[0].name)); + strlcpy(current_node->volumes[vi].path, (yyvsp[(3) - (3)].str), + sizeof(current_node->volumes[0].path)); + current_node->volumeCount++; } + free((yyvsp[(2) - (3)].str)); + free((yyvsp[(3) - (3)].str)); break; + } case 79: #line 614 "test_spec_parse.y" + { + /* volume "/path/with spaces" */ + int vi = current_node->volumeCount; + if (vi < PGAF_MAX_NODE_VOLUMES) { - /* volume "/path/with spaces" */ - int vi = current_node->volumeCount; - if (vi < PGAF_MAX_NODE_VOLUMES) - { - strlcpy(current_node->volumes[vi].name, (yyvsp[(2) - (3)].str), - sizeof(current_node->volumes[0].name)); - strlcpy(current_node->volumes[vi].path, (yyvsp[(3) - (3)].str), - sizeof(current_node->volumes[0].path)); - current_node->volumeCount++; - } - free((yyvsp[(2) - (3)].str)); - free((yyvsp[(3) - (3)].str)); + strlcpy(current_node->volumes[vi].name, (yyvsp[(2) - (3)].str), + sizeof(current_node->volumes[0].name)); + strlcpy(current_node->volumes[vi].path, (yyvsp[(3) - (3)].str), + sizeof(current_node->volumes[0].path)); + current_node->volumeCount++; } + free((yyvsp[(2) - (3)].str)); + free((yyvsp[(3) - (3)].str)); break; + } case 80: #line 635 "test_spec_parse.y" - { - current_spec->setup = (yyvsp[(2) - (2)].step); - } + { + current_spec->setup = (yyvsp[(2) - (2)].step); break; + } case 81: #line 642 "test_spec_parse.y" - { - current_spec->teardown = (yyvsp[(2) - (2)].step); - } + { + current_spec->teardown = (yyvsp[(2) - (2)].step); break; + } case 82: #line 653 "test_spec_parse.y" - { - TestStep *s = (yyvsp[(3) - (3)].step); - strlcpy(s->name, (yyvsp[(2) - (3)].str), sizeof(s->name)); - free((yyvsp[(2) - (3)].str)); - register_step(current_spec, s); - } + { + TestStep *s = (yyvsp[(3) - (3)].step); + strlcpy(s->name, (yyvsp[(2) - (3)].str), sizeof(s->name)); + free((yyvsp[(2) - (3)].str)); + register_step(current_spec, s); break; + } case 83: #line 671 "test_spec_parse.y" + { + /* post-process: CMD_SQL immediately before CMD_EXPECT_ERROR */ + for (TestCmd *c = (yyvsp[(2) - (3)].step)->commands; c; c = c->next) { - /* post-process: CMD_SQL immediately before CMD_EXPECT_ERROR */ - for (TestCmd *c = (yyvsp[(2) - (3)].step)->commands; c; c = c->next) + if (c->kind == CMD_SQL && c->next && + c->next->kind == CMD_EXPECT_ERROR) { - if (c->kind == CMD_SQL && c->next && - c->next->kind == CMD_EXPECT_ERROR) - { - c->allowError = true; - } + c->allowError = true; } - (yyval.step) = (yyvsp[(2) - (3)].step); } + (yyval.step) = (yyvsp[(2) - (3)].step); break; + } case 84: #line 685 "test_spec_parse.y" - { - (yyval.step) = make_step(""); - } + { + (yyval.step) = make_step(""); break; + } case 85: #line 689 "test_spec_parse.y" + { + if ((yyvsp[(2) - (2)].cmd)) { - if ((yyvsp[(2) - (2)].cmd)) - { - append_cmd((yyvsp[(1) - (2)].step), (yyvsp[(2) - (2)].cmd)); - } - (yyval.step) = (yyvsp[(1) - (2)].step); + append_cmd((yyvsp[(1) - (2)].step), (yyvsp[(2) - (2)].cmd)); } + (yyval.step) = (yyvsp[(1) - (2)].step); break; + } case 86: #line 696 "test_spec_parse.y" - { - (yyval.cmd) = (yyvsp[(1) - (1)].cmd); - } + { + (yyval.cmd) = (yyvsp[(1) - (1)].cmd); break; + } case 87: #line 697 "test_spec_parse.y" - { - (yyval.cmd) = (yyvsp[(1) - (1)].cmd); - } + { + (yyval.cmd) = (yyvsp[(1) - (1)].cmd); break; + } case 88: #line 698 "test_spec_parse.y" - { - (yyval.cmd) = (yyvsp[(1) - (1)].cmd); - } + { + (yyval.cmd) = (yyvsp[(1) - (1)].cmd); break; + } case 89: #line 699 "test_spec_parse.y" - { - (yyval.cmd) = (yyvsp[(1) - (1)].cmd); - } + { + (yyval.cmd) = (yyvsp[(1) - (1)].cmd); break; + } case 90: #line 700 "test_spec_parse.y" - { - (yyval.cmd) = (yyvsp[(1) - (1)].cmd); - } + { + (yyval.cmd) = (yyvsp[(1) - (1)].cmd); break; + } case 91: #line 701 "test_spec_parse.y" - { - (yyval.cmd) = (yyvsp[(1) - (1)].cmd); - } + { + (yyval.cmd) = (yyvsp[(1) - (1)].cmd); break; + } case 92: #line 702 "test_spec_parse.y" - { - (yyval.cmd) = (yyvsp[(1) - (1)].cmd); - } + { + (yyval.cmd) = (yyvsp[(1) - (1)].cmd); break; + } case 93: #line 703 "test_spec_parse.y" - { - (yyval.cmd) = (yyvsp[(1) - (1)].cmd); - } + { + (yyval.cmd) = (yyvsp[(1) - (1)].cmd); break; + } case 94: #line 704 "test_spec_parse.y" - { - (yyval.cmd) = (yyvsp[(1) - (1)].cmd); - } + { + (yyval.cmd) = (yyvsp[(1) - (1)].cmd); break; + } case 95: #line 705 "test_spec_parse.y" - { - (yyval.cmd) = (yyvsp[(1) - (1)].cmd); - } + { + (yyval.cmd) = (yyvsp[(1) - (1)].cmd); break; + } case 96: #line 706 "test_spec_parse.y" - { - (yyval.cmd) = (yyvsp[(1) - (1)].cmd); - } + { + (yyval.cmd) = (yyvsp[(1) - (1)].cmd); break; + } case 97: #line 707 "test_spec_parse.y" - { - (yyval.cmd) = (yyvsp[(1) - (1)].cmd); - } + { + (yyval.cmd) = (yyvsp[(1) - (1)].cmd); break; + } case 98: #line 708 "test_spec_parse.y" - { - (yyval.cmd) = (yyvsp[(1) - (1)].cmd); - } + { + (yyval.cmd) = (yyvsp[(1) - (1)].cmd); break; + } case 99: #line 723 "test_spec_parse.y" - { - (yyval.cmd) = make_cmd(CMD_EXEC); - strlcpy((yyval.cmd)->service, (yyvsp[(2) - (3)].str), - sizeof((yyval.cmd)->service)); - strlcpy((yyval.cmd)->args, (yyvsp[(3) - (3)].str), - sizeof((yyval.cmd)->args)); - free((yyvsp[(2) - (3)].str)); - free((yyvsp[(3) - (3)].str)); - } + { + (yyval.cmd) = make_cmd(CMD_EXEC); + strlcpy((yyval.cmd)->service, (yyvsp[(2) - (3)].str), + sizeof((yyval.cmd)->service)); + strlcpy((yyval.cmd)->args, (yyvsp[(3) - (3)].str), + sizeof((yyval.cmd)->args)); + free((yyvsp[(2) - (3)].str)); + free((yyvsp[(3) - (3)].str)); break; + } case 100: #line 730 "test_spec_parse.y" - { - (yyval.cmd) = make_cmd(CMD_EXEC); - strlcpy((yyval.cmd)->service, (yyvsp[(2) - (2)].str), - sizeof((yyval.cmd)->service)); - free((yyvsp[(2) - (2)].str)); - } + { + (yyval.cmd) = make_cmd(CMD_EXEC); + strlcpy((yyval.cmd)->service, (yyvsp[(2) - (2)].str), + sizeof((yyval.cmd)->service)); + free((yyvsp[(2) - (2)].str)); break; + } case 101: #line 736 "test_spec_parse.y" - { - (yyval.cmd) = make_cmd(CMD_EXEC_FAILS); - strlcpy((yyval.cmd)->service, (yyvsp[(2) - (3)].str), - sizeof((yyval.cmd)->service)); - strlcpy((yyval.cmd)->args, (yyvsp[(3) - (3)].str), - sizeof((yyval.cmd)->args)); - free((yyvsp[(2) - (3)].str)); - free((yyvsp[(3) - (3)].str)); - } + { + (yyval.cmd) = make_cmd(CMD_EXEC_FAILS); + strlcpy((yyval.cmd)->service, (yyvsp[(2) - (3)].str), + sizeof((yyval.cmd)->service)); + strlcpy((yyval.cmd)->args, (yyvsp[(3) - (3)].str), + sizeof((yyval.cmd)->args)); + free((yyvsp[(2) - (3)].str)); + free((yyvsp[(3) - (3)].str)); break; + } case 102: #line 743 "test_spec_parse.y" - { - (yyval.cmd) = make_cmd(CMD_EXEC_FAILS); - strlcpy((yyval.cmd)->service, (yyvsp[(2) - (2)].str), - sizeof((yyval.cmd)->service)); - free((yyvsp[(2) - (2)].str)); - } + { + (yyval.cmd) = make_cmd(CMD_EXEC_FAILS); + strlcpy((yyval.cmd)->service, (yyvsp[(2) - (2)].str), + sizeof((yyval.cmd)->service)); + free((yyvsp[(2) - (2)].str)); break; + } case 103: #line 749 "test_spec_parse.y" - { - /* "pg_autoctl perform failover --formation auth" - * EXEC_ARGS returns T_IDENT for first word, T_SHELL_ARGS for rest */ - (yyval.cmd) = make_cmd(CMD_PG_AUTOCTL); - sformat((yyval.cmd)->args, sizeof((yyval.cmd)->args), "%s %s", - (yyvsp[(2) - (3)].str), (yyvsp[(3) - (3)].str)); - free((yyvsp[(2) - (3)].str)); - free((yyvsp[(3) - (3)].str)); - } + { + /* "pg_autoctl perform failover --formation auth" + * EXEC_ARGS returns T_IDENT for first word, T_SHELL_ARGS for rest */ + (yyval.cmd) = make_cmd(CMD_PG_AUTOCTL); + sformat((yyval.cmd)->args, sizeof((yyval.cmd)->args), "%s %s", + (yyvsp[(2) - (3)].str), (yyvsp[(3) - (3)].str)); + free((yyvsp[(2) - (3)].str)); + free((yyvsp[(3) - (3)].str)); break; + } case 104: #line 757 "test_spec_parse.y" - { - (yyval.cmd) = make_cmd(CMD_PG_AUTOCTL); - strlcpy((yyval.cmd)->args, (yyvsp[(2) - (2)].str), - sizeof((yyval.cmd)->args)); - free((yyvsp[(2) - (2)].str)); - } + { + (yyval.cmd) = make_cmd(CMD_PG_AUTOCTL); + strlcpy((yyval.cmd)->args, (yyvsp[(2) - (2)].str), + sizeof((yyval.cmd)->args)); + free((yyvsp[(2) - (2)].str)); break; + } case 105: #line 763 "test_spec_parse.y" - { - (yyval.cmd) = make_cmd(CMD_PG_AUTOCTL); - } + { + (yyval.cmd) = make_cmd(CMD_PG_AUTOCTL); break; + } case 108: #line 801 "test_spec_parse.y" + { + if (!current_wait_cmd) { - if (!current_wait_cmd) - { - current_wait_cmd = make_cmd(CMD_WAIT_MULTI); - } - int i = current_wait_cmd->waitStateCount; - if (i < PGAF_MAX_WAIT_STATES) - { - strlcpy(current_wait_cmd->waitNodes[i], (yyvsp[(1) - (4)].str), - sizeof(current_wait_cmd->waitNodes[0])); - strlcpy(current_wait_cmd->waitStates[i], (yyvsp[(4) - (4)].str), - sizeof(current_wait_cmd->waitStates[0])); - current_wait_cmd->waitStateCount++; - } - free((yyvsp[(1) - (4)].str)); + current_wait_cmd = make_cmd(CMD_WAIT_MULTI); + } + int i = current_wait_cmd->waitStateCount; + if (i < PGAF_MAX_WAIT_STATES) + { + strlcpy(current_wait_cmd->waitNodes[i], (yyvsp[(1) - (4)].str), + sizeof(current_wait_cmd->waitNodes[0])); + strlcpy(current_wait_cmd->waitStates[i], (yyvsp[(4) - (4)].str), + sizeof(current_wait_cmd->waitStates[0])); + current_wait_cmd->waitStateCount++; } + free((yyvsp[(1) - (4)].str)); break; + } case 109: #line 816 "test_spec_parse.y" + { + if (!current_wait_cmd) { - if (!current_wait_cmd) - { - current_wait_cmd = make_cmd(CMD_WAIT_MULTI); - } - int i = current_wait_cmd->waitStateCount; - if (i < PGAF_MAX_WAIT_STATES) - { - strlcpy(current_wait_cmd->waitNodes[i], (yyvsp[(1) - (4)].str), - sizeof(current_wait_cmd->waitNodes[0])); - strlcpy(current_wait_cmd->waitStates[i], (yyvsp[(4) - (4)].str), - sizeof(current_wait_cmd->waitStates[0])); - current_wait_cmd->waitStateCount++; - } - free((yyvsp[(1) - (4)].str)); - free((yyvsp[(4) - (4)].str)); + current_wait_cmd = make_cmd(CMD_WAIT_MULTI); + } + int i = current_wait_cmd->waitStateCount; + if (i < PGAF_MAX_WAIT_STATES) + { + strlcpy(current_wait_cmd->waitNodes[i], (yyvsp[(1) - (4)].str), + sizeof(current_wait_cmd->waitNodes[0])); + strlcpy(current_wait_cmd->waitStates[i], (yyvsp[(4) - (4)].str), + sizeof(current_wait_cmd->waitStates[0])); + current_wait_cmd->waitStateCount++; } + free((yyvsp[(1) - (4)].str)); + free((yyvsp[(4) - (4)].str)); break; + } case 114: #line 856 "test_spec_parse.y" + { + /* current_pass_cmd set by the enclosing wait_cmd rule */ + if (current_pass_cmd && + current_pass_cmd->passThroughCount < PGAF_MAX_WAIT_STATES) { - /* current_pass_cmd set by the enclosing wait_cmd rule */ - if (current_pass_cmd && - current_pass_cmd->passThroughCount < PGAF_MAX_WAIT_STATES) - { - strlcpy( - current_pass_cmd->passThroughStates[current_pass_cmd-> - passThroughCount++], - (yyvsp[(1) - (1)].str), - sizeof(current_pass_cmd->passThroughStates[0])); - } + strlcpy( + current_pass_cmd->passThroughStates[current_pass_cmd-> + passThroughCount++], + (yyvsp[(1) - (1)].str), + sizeof(current_pass_cmd->passThroughStates[0])); } break; + } case 115: #line 864 "test_spec_parse.y" + { + if (current_pass_cmd && + current_pass_cmd->passThroughCount < PGAF_MAX_WAIT_STATES) { - if (current_pass_cmd && - current_pass_cmd->passThroughCount < PGAF_MAX_WAIT_STATES) - { - strlcpy( - current_pass_cmd->passThroughStates[current_pass_cmd-> - passThroughCount++], - (yyvsp[(1) - (1)].str), - sizeof(current_pass_cmd->passThroughStates[0])); - } - free((yyvsp[(1) - (1)].str)); + strlcpy( + current_pass_cmd->passThroughStates[current_pass_cmd-> + passThroughCount++], + (yyvsp[(1) - (1)].str), + sizeof(current_pass_cmd->passThroughStates[0])); } + free((yyvsp[(1) - (1)].str)); break; + } case 116: #line 872 "test_spec_parse.y" + { + if (current_pass_cmd && + current_pass_cmd->passThroughCount < PGAF_MAX_WAIT_STATES) { - if (current_pass_cmd && - current_pass_cmd->passThroughCount < PGAF_MAX_WAIT_STATES) - { - strlcpy( - current_pass_cmd->passThroughStates[current_pass_cmd-> - passThroughCount++], - (yyvsp[(3) - (3)].str), - sizeof(current_pass_cmd->passThroughStates[0])); - } + strlcpy( + current_pass_cmd->passThroughStates[current_pass_cmd-> + passThroughCount++], + (yyvsp[(3) - (3)].str), + sizeof(current_pass_cmd->passThroughStates[0])); } break; + } case 117: #line 879 "test_spec_parse.y" + { + if (current_pass_cmd && + current_pass_cmd->passThroughCount < PGAF_MAX_WAIT_STATES) { - if (current_pass_cmd && - current_pass_cmd->passThroughCount < PGAF_MAX_WAIT_STATES) - { - strlcpy( - current_pass_cmd->passThroughStates[current_pass_cmd-> - passThroughCount++], - (yyvsp[(3) - (3)].str), - sizeof(current_pass_cmd->passThroughStates[0])); - } - free((yyvsp[(3) - (3)].str)); + strlcpy( + current_pass_cmd->passThroughStates[current_pass_cmd-> + passThroughCount++], + (yyvsp[(3) - (3)].str), + sizeof(current_pass_cmd->passThroughStates[0])); } + free((yyvsp[(3) - (3)].str)); break; + } case 118: #line 890 "test_spec_parse.y" - { - current_pass_cmd = make_cmd(CMD_WAIT_STATE); - strlcpy(current_pass_cmd->service, (yyvsp[(3) - (6)].str), - sizeof(current_pass_cmd->service)); - strlcpy(current_pass_cmd->state, (yyvsp[(6) - (6)].str), - sizeof(current_pass_cmd->state)); - free((yyvsp[(3) - (6)].str)); - } + { + current_pass_cmd = make_cmd(CMD_WAIT_STATE); + strlcpy(current_pass_cmd->service, (yyvsp[(3) - (6)].str), + sizeof(current_pass_cmd->service)); + strlcpy(current_pass_cmd->state, (yyvsp[(6) - (6)].str), + sizeof(current_pass_cmd->state)); + free((yyvsp[(3) - (6)].str)); break; + } case 119: #line 895 "test_spec_parse.y" - { - current_pass_cmd->timeoutSeconds = (yyvsp[(9) - (9)].ival); - (yyval.cmd) = current_pass_cmd; - current_pass_cmd = NULL; - } + { + current_pass_cmd->timeoutSeconds = (yyvsp[(9) - (9)].ival); + (yyval.cmd) = current_pass_cmd; + current_pass_cmd = NULL; break; + } case 120: #line 901 "test_spec_parse.y" - { - current_pass_cmd = make_cmd(CMD_WAIT_STATE); - strlcpy(current_pass_cmd->service, (yyvsp[(3) - (6)].str), - sizeof(current_pass_cmd->service)); - strlcpy(current_pass_cmd->state, (yyvsp[(6) - (6)].str), - sizeof(current_pass_cmd->state)); - free((yyvsp[(3) - (6)].str)); - free((yyvsp[(6) - (6)].str)); - } + { + current_pass_cmd = make_cmd(CMD_WAIT_STATE); + strlcpy(current_pass_cmd->service, (yyvsp[(3) - (6)].str), + sizeof(current_pass_cmd->service)); + strlcpy(current_pass_cmd->state, (yyvsp[(6) - (6)].str), + sizeof(current_pass_cmd->state)); + free((yyvsp[(3) - (6)].str)); + free((yyvsp[(6) - (6)].str)); break; + } case 121: #line 906 "test_spec_parse.y" - { - current_pass_cmd->timeoutSeconds = (yyvsp[(9) - (9)].ival); - (yyval.cmd) = current_pass_cmd; - current_pass_cmd = NULL; - } + { + current_pass_cmd->timeoutSeconds = (yyvsp[(9) - (9)].ival); + (yyval.cmd) = current_pass_cmd; + current_pass_cmd = NULL; break; + } case 122: #line 912 "test_spec_parse.y" - { - (yyval.cmd) = make_cmd(CMD_WAIT_STATE); - (yyval.cmd)->kind = CMD_ASSERT_ASSIGNED; - strlcpy((yyval.cmd)->service, (yyvsp[(3) - (7)].str), - sizeof((yyval.cmd)->service)); - strlcpy((yyval.cmd)->state, (yyvsp[(6) - (7)].str), - sizeof((yyval.cmd)->state)); - (yyval.cmd)->timeoutSeconds = (yyvsp[(7) - (7)].ival); - free((yyvsp[(3) - (7)].str)); - } + { + (yyval.cmd) = make_cmd(CMD_WAIT_STATE); + (yyval.cmd)->kind = CMD_ASSERT_ASSIGNED; + strlcpy((yyval.cmd)->service, (yyvsp[(3) - (7)].str), + sizeof((yyval.cmd)->service)); + strlcpy((yyval.cmd)->state, (yyvsp[(6) - (7)].str), + sizeof((yyval.cmd)->state)); + (yyval.cmd)->timeoutSeconds = (yyvsp[(7) - (7)].ival); + free((yyvsp[(3) - (7)].str)); break; + } case 123: #line 921 "test_spec_parse.y" - { - (yyval.cmd) = make_cmd(CMD_WAIT_STATE); - (yyval.cmd)->kind = CMD_ASSERT_ASSIGNED; - strlcpy((yyval.cmd)->service, (yyvsp[(3) - (7)].str), - sizeof((yyval.cmd)->service)); - strlcpy((yyval.cmd)->state, (yyvsp[(6) - (7)].str), - sizeof((yyval.cmd)->state)); - (yyval.cmd)->timeoutSeconds = (yyvsp[(7) - (7)].ival); - free((yyvsp[(3) - (7)].str)); - free((yyvsp[(6) - (7)].str)); - } + { + (yyval.cmd) = make_cmd(CMD_WAIT_STATE); + (yyval.cmd)->kind = CMD_ASSERT_ASSIGNED; + strlcpy((yyval.cmd)->service, (yyvsp[(3) - (7)].str), + sizeof((yyval.cmd)->service)); + strlcpy((yyval.cmd)->state, (yyvsp[(6) - (7)].str), + sizeof((yyval.cmd)->state)); + (yyval.cmd)->timeoutSeconds = (yyvsp[(7) - (7)].ival); + free((yyvsp[(3) - (7)].str)); + free((yyvsp[(6) - (7)].str)); break; + } case 124: #line 930 "test_spec_parse.y" - { - (yyval.cmd) = make_cmd(CMD_WAIT_STOPPED); - strlcpy((yyval.cmd)->service, (yyvsp[(3) - (5)].str), - sizeof((yyval.cmd)->service)); - (yyval.cmd)->timeoutSeconds = (yyvsp[(5) - (5)].ival); - free((yyvsp[(3) - (5)].str)); - } + { + (yyval.cmd) = make_cmd(CMD_WAIT_STOPPED); + strlcpy((yyval.cmd)->service, (yyvsp[(3) - (5)].str), + sizeof((yyval.cmd)->service)); + (yyval.cmd)->timeoutSeconds = (yyvsp[(5) - (5)].ival); + free((yyvsp[(3) - (5)].str)); break; + } case 125: #line 937 "test_spec_parse.y" - { - (yyval.cmd) = current_wait_cmd; - (yyval.cmd)->timeoutSeconds = (yyvsp[(5) - (5)].ival); - current_wait_cmd = NULL; - } + { + (yyval.cmd) = current_wait_cmd; + (yyval.cmd)->timeoutSeconds = (yyvsp[(5) - (5)].ival); + current_wait_cmd = NULL; break; + } case 126: #line 951 "test_spec_parse.y" - { - (yyval.cmd) = current_wait_cmd; - (yyval.cmd)->timeoutSeconds = (yyvsp[(6) - (6)].ival); - current_wait_cmd = NULL; - } + { + (yyval.cmd) = current_wait_cmd; + (yyval.cmd)->timeoutSeconds = (yyvsp[(6) - (6)].ival); + current_wait_cmd = NULL; break; + } case 127: #line 966 "test_spec_parse.y" - { - current_wait_cmd = make_cmd(CMD_WAIT_STATES); - strlcpy(current_wait_cmd->waitStates[current_wait_cmd->waitStateCount++], - (yyvsp[(1) - (1)].str), sizeof(current_wait_cmd->waitStates[0])); - } + { + current_wait_cmd = make_cmd(CMD_WAIT_STATES); + strlcpy(current_wait_cmd->waitStates[current_wait_cmd->waitStateCount++], + (yyvsp[(1) - (1)].str), sizeof(current_wait_cmd->waitStates[0])); break; + } case 128: #line 972 "test_spec_parse.y" - { - current_wait_cmd = make_cmd(CMD_WAIT_STATES); - strlcpy(current_wait_cmd->waitStates[current_wait_cmd->waitStateCount++], - (yyvsp[(1) - (1)].str), sizeof(current_wait_cmd->waitStates[0])); - free((yyvsp[(1) - (1)].str)); - } + { + current_wait_cmd = make_cmd(CMD_WAIT_STATES); + strlcpy(current_wait_cmd->waitStates[current_wait_cmd->waitStateCount++], + (yyvsp[(1) - (1)].str), sizeof(current_wait_cmd->waitStates[0])); + free((yyvsp[(1) - (1)].str)); break; + } case 129: #line 979 "test_spec_parse.y" + { + if (current_wait_cmd->waitStateCount < PGAF_MAX_WAIT_STATES) { - if (current_wait_cmd->waitStateCount < PGAF_MAX_WAIT_STATES) - { - strlcpy( - current_wait_cmd->waitStates[current_wait_cmd->waitStateCount++], - (yyvsp[(3) - (3)].str), - sizeof(current_wait_cmd->waitStates[0])); - } + strlcpy( + current_wait_cmd->waitStates[current_wait_cmd->waitStateCount++], + (yyvsp[(3) - (3)].str), + sizeof(current_wait_cmd->waitStates[0])); } break; + } case 130: #line 985 "test_spec_parse.y" + { + if (current_wait_cmd->waitStateCount < PGAF_MAX_WAIT_STATES) { - if (current_wait_cmd->waitStateCount < PGAF_MAX_WAIT_STATES) - { - strlcpy( - current_wait_cmd->waitStates[current_wait_cmd->waitStateCount++], - (yyvsp[(3) - (3)].str), - sizeof(current_wait_cmd->waitStates[0])); - } - free((yyvsp[(3) - (3)].str)); + strlcpy( + current_wait_cmd->waitStates[current_wait_cmd->waitStateCount++], + (yyvsp[(3) - (3)].str), + sizeof(current_wait_cmd->waitStates[0])); } + free((yyvsp[(3) - (3)].str)); break; + } case 133: #line 1004 "test_spec_parse.y" + { + if (current_wait_cmd->waitGroupCount < PGAF_MAX_WAIT_GROUPS) { - if (current_wait_cmd->waitGroupCount < PGAF_MAX_WAIT_GROUPS) - { - current_wait_cmd->waitGroups[current_wait_cmd->waitGroupCount++] = - (yyvsp[(2) - (2)].ival); - } + current_wait_cmd->waitGroups[current_wait_cmd->waitGroupCount++] = + (yyvsp[(2) - (2)].ival); } break; + } case 134: #line 1009 "test_spec_parse.y" + { + if (current_wait_cmd->waitGroupCount < PGAF_MAX_WAIT_GROUPS) { - if (current_wait_cmd->waitGroupCount < PGAF_MAX_WAIT_GROUPS) - { - current_wait_cmd->waitGroups[current_wait_cmd->waitGroupCount++] = - (yyvsp[(4) - (4)].ival); - } + current_wait_cmd->waitGroups[current_wait_cmd->waitGroupCount++] = + (yyvsp[(4) - (4)].ival); } break; + } case 135: #line 1016 "test_spec_parse.y" - { - (yyval.ival) = PGAF_TIMEOUT_DEFAULT; - } + { + (yyval.ival) = PGAF_TIMEOUT_DEFAULT; break; + } case 136: #line 1017 "test_spec_parse.y" - { - (yyval.ival) = (yyvsp[(2) - (2)].ival); - } + { + (yyval.ival) = (yyvsp[(2) - (2)].ival); break; + } case 137: #line 1018 "test_spec_parse.y" - { - (yyval.ival) = (yyvsp[(3) - (3)].ival); - } + { + (yyval.ival) = (yyvsp[(3) - (3)].ival); break; + } case 138: #line 1030 "test_spec_parse.y" - { - (yyval.cmd) = make_cmd((yyvsp[(6) - (6)].ival) > 0 ? CMD_WAIT_STATE : - CMD_ASSERT_STATE); - strlcpy((yyval.cmd)->service, (yyvsp[(2) - (6)].str), - sizeof((yyval.cmd)->service)); - strlcpy((yyval.cmd)->state, (yyvsp[(5) - (6)].str), - sizeof((yyval.cmd)->state)); - (yyval.cmd)->timeoutSeconds = (yyvsp[(6) - (6)].ival); - free((yyvsp[(2) - (6)].str)); - } + { + (yyval.cmd) = make_cmd((yyvsp[(6) - (6)].ival) > 0 ? CMD_WAIT_STATE : + CMD_ASSERT_STATE); + strlcpy((yyval.cmd)->service, (yyvsp[(2) - (6)].str), + sizeof((yyval.cmd)->service)); + strlcpy((yyval.cmd)->state, (yyvsp[(5) - (6)].str), + sizeof((yyval.cmd)->state)); + (yyval.cmd)->timeoutSeconds = (yyvsp[(6) - (6)].ival); + free((yyvsp[(2) - (6)].str)); break; + } case 139: #line 1038 "test_spec_parse.y" - { - (yyval.cmd) = make_cmd((yyvsp[(6) - (6)].ival) > 0 ? CMD_WAIT_STATE : - CMD_ASSERT_STATE); - strlcpy((yyval.cmd)->service, (yyvsp[(2) - (6)].str), - sizeof((yyval.cmd)->service)); - strlcpy((yyval.cmd)->state, (yyvsp[(5) - (6)].str), - sizeof((yyval.cmd)->state)); - (yyval.cmd)->timeoutSeconds = (yyvsp[(6) - (6)].ival); - free((yyvsp[(2) - (6)].str)); - free((yyvsp[(5) - (6)].str)); - } + { + (yyval.cmd) = make_cmd((yyvsp[(6) - (6)].ival) > 0 ? CMD_WAIT_STATE : + CMD_ASSERT_STATE); + strlcpy((yyval.cmd)->service, (yyvsp[(2) - (6)].str), + sizeof((yyval.cmd)->service)); + strlcpy((yyval.cmd)->state, (yyvsp[(5) - (6)].str), + sizeof((yyval.cmd)->state)); + (yyval.cmd)->timeoutSeconds = (yyvsp[(6) - (6)].ival); + free((yyvsp[(2) - (6)].str)); + free((yyvsp[(5) - (6)].str)); break; + } case 140: #line 1046 "test_spec_parse.y" - { - (yyval.cmd) = make_cmd(CMD_ASSERT_ASSIGNED); - strlcpy((yyval.cmd)->service, (yyvsp[(2) - (6)].str), - sizeof((yyval.cmd)->service)); - strlcpy((yyval.cmd)->state, (yyvsp[(5) - (6)].str), - sizeof((yyval.cmd)->state)); - (yyval.cmd)->timeoutSeconds = (yyvsp[(6) - (6)].ival); - free((yyvsp[(2) - (6)].str)); - } + { + (yyval.cmd) = make_cmd(CMD_ASSERT_ASSIGNED); + strlcpy((yyval.cmd)->service, (yyvsp[(2) - (6)].str), + sizeof((yyval.cmd)->service)); + strlcpy((yyval.cmd)->state, (yyvsp[(5) - (6)].str), + sizeof((yyval.cmd)->state)); + (yyval.cmd)->timeoutSeconds = (yyvsp[(6) - (6)].ival); + free((yyvsp[(2) - (6)].str)); break; + } case 141: #line 1054 "test_spec_parse.y" - { - (yyval.cmd) = make_cmd(CMD_ASSERT_ASSIGNED); - strlcpy((yyval.cmd)->service, (yyvsp[(2) - (6)].str), - sizeof((yyval.cmd)->service)); - strlcpy((yyval.cmd)->state, (yyvsp[(5) - (6)].str), - sizeof((yyval.cmd)->state)); - (yyval.cmd)->timeoutSeconds = (yyvsp[(6) - (6)].ival); - free((yyvsp[(2) - (6)].str)); - free((yyvsp[(5) - (6)].str)); - } + { + (yyval.cmd) = make_cmd(CMD_ASSERT_ASSIGNED); + strlcpy((yyval.cmd)->service, (yyvsp[(2) - (6)].str), + sizeof((yyval.cmd)->service)); + strlcpy((yyval.cmd)->state, (yyvsp[(5) - (6)].str), + sizeof((yyval.cmd)->state)); + (yyval.cmd)->timeoutSeconds = (yyvsp[(6) - (6)].ival); + free((yyvsp[(2) - (6)].str)); + free((yyvsp[(5) - (6)].str)); break; + } case 142: #line 1072 "test_spec_parse.y" - { - (yyval.cmd) = make_cmd(CMD_SQL); - strlcpy((yyval.cmd)->service, (yyvsp[(2) - (3)].str), - sizeof((yyval.cmd)->service)); - strlcpy((yyval.cmd)->args, (yyvsp[(3) - (3)].str), - sizeof((yyval.cmd)->args)); - free((yyvsp[(2) - (3)].str)); - free((yyvsp[(3) - (3)].str)); - } + { + (yyval.cmd) = make_cmd(CMD_SQL); + strlcpy((yyval.cmd)->service, (yyvsp[(2) - (3)].str), + sizeof((yyval.cmd)->service)); + strlcpy((yyval.cmd)->args, (yyvsp[(3) - (3)].str), + sizeof((yyval.cmd)->args)); + free((yyvsp[(2) - (3)].str)); + free((yyvsp[(3) - (3)].str)); break; + } case 143: #line 1087 "test_spec_parse.y" - { - (yyval.cmd) = make_cmd(CMD_EXPECT); - strlcpy((yyval.cmd)->expected, (yyvsp[(2) - (2)].str), - sizeof((yyval.cmd)->expected)); - expand_tuple_expect((yyval.cmd)->expected, sizeof((yyval.cmd)->expected)); - free((yyvsp[(2) - (2)].str)); - } + { + (yyval.cmd) = make_cmd(CMD_EXPECT); + strlcpy((yyval.cmd)->expected, (yyvsp[(2) - (2)].str), + sizeof((yyval.cmd)->expected)); + expand_tuple_expect((yyval.cmd)->expected, sizeof((yyval.cmd)->expected)); + free((yyvsp[(2) - (2)].str)); break; + } case 144: #line 1094 "test_spec_parse.y" - { - (yyval.cmd) = make_cmd(CMD_EXPECT_ERROR); - } + { + (yyval.cmd) = make_cmd(CMD_EXPECT_ERROR); break; + } case 145: #line 1098 "test_spec_parse.y" - { - (yyval.cmd) = make_cmd(CMD_EXPECT_ERROR); - strlcpy((yyval.cmd)->state, (yyvsp[(3) - (3)].str), - sizeof((yyval.cmd)->state)); - free((yyvsp[(3) - (3)].str)); - } + { + (yyval.cmd) = make_cmd(CMD_EXPECT_ERROR); + strlcpy((yyval.cmd)->state, (yyvsp[(3) - (3)].str), + sizeof((yyval.cmd)->state)); + free((yyvsp[(3) - (3)].str)); break; + } case 146: #line 1104 "test_spec_parse.y" - { - /* SQLSTATE codes like 25006 are all digits, lexed as T_INTEGER */ - (yyval.cmd) = make_cmd(CMD_EXPECT_ERROR); - snprintf(/* IGNORE-BANNED */ (yyval.cmd)->state, - sizeof((yyval.cmd)->state), "%d", - (yyvsp[(3) - (3)].ival)); - } + { + /* SQLSTATE codes like 25006 are all digits, lexed as T_INTEGER */ + (yyval.cmd) = make_cmd(CMD_EXPECT_ERROR); + snprintf(/* IGNORE-BANNED */ (yyval.cmd)->state, + sizeof((yyval.cmd)->state), "%d", + (yyvsp[(3) - (3)].ival)); break; + } case 147: #line 1117 "test_spec_parse.y" - { - (yyval.cmd) = current_promote_cmd; - current_promote_cmd = NULL; - } + { + (yyval.cmd) = current_promote_cmd; + current_promote_cmd = NULL; break; + } case 148: #line 1125 "test_spec_parse.y" - { - current_promote_cmd = make_cmd(CMD_PROMOTE); - current_promote_cmd->timeoutSeconds = PGAF_TIMEOUT_DEFAULT; - strlcpy( - current_promote_cmd->promoteNodes[current_promote_cmd->promoteCount++], - (yyvsp[(1) - (1)].str), - sizeof(current_promote_cmd->promoteNodes[0])); - free((yyvsp[(1) - (1)].str)); - } + { + current_promote_cmd = make_cmd(CMD_PROMOTE); + current_promote_cmd->timeoutSeconds = PGAF_TIMEOUT_DEFAULT; + strlcpy( + current_promote_cmd->promoteNodes[current_promote_cmd->promoteCount++], + (yyvsp[(1) - (1)].str), + sizeof(current_promote_cmd->promoteNodes[0])); + free((yyvsp[(1) - (1)].str)); break; + } case 149: #line 1133 "test_spec_parse.y" + { + if (current_promote_cmd->promoteCount < PGAF_MAX_PROMOTE_NODES) { - if (current_promote_cmd->promoteCount < PGAF_MAX_PROMOTE_NODES) - { - strlcpy( - current_promote_cmd->promoteNodes[current_promote_cmd-> - promoteCount++], - (yyvsp[(3) - (3)].str), - sizeof(current_promote_cmd->promoteNodes[0])); - } - free((yyvsp[(3) - (3)].str)); + strlcpy( + current_promote_cmd->promoteNodes[current_promote_cmd-> + promoteCount++], + (yyvsp[(3) - (3)].str), + sizeof(current_promote_cmd->promoteNodes[0])); } + free((yyvsp[(3) - (3)].str)); break; + } case 150: #line 1148 "test_spec_parse.y" - { - (yyval.cmd) = make_cmd(CMD_NETWORK_OFF); - strlcpy((yyval.cmd)->service, (yyvsp[(3) - (3)].str), - sizeof((yyval.cmd)->service)); - free((yyvsp[(3) - (3)].str)); - } + { + (yyval.cmd) = make_cmd(CMD_NETWORK_OFF); + strlcpy((yyval.cmd)->service, (yyvsp[(3) - (3)].str), + sizeof((yyval.cmd)->service)); + free((yyvsp[(3) - (3)].str)); break; + } case 151: #line 1154 "test_spec_parse.y" - { - (yyval.cmd) = make_cmd(CMD_NETWORK_ON); - strlcpy((yyval.cmd)->service, (yyvsp[(3) - (3)].str), - sizeof((yyval.cmd)->service)); - free((yyvsp[(3) - (3)].str)); - } + { + (yyval.cmd) = make_cmd(CMD_NETWORK_ON); + strlcpy((yyval.cmd)->service, (yyvsp[(3) - (3)].str), + sizeof((yyval.cmd)->service)); + free((yyvsp[(3) - (3)].str)); break; + } case 152: #line 1167 "test_spec_parse.y" - { - (yyval.cmd) = make_cmd(CMD_SLEEP); - (yyval.cmd)->timeoutSeconds = (yyvsp[(2) - (2)].ival); - } + { + (yyval.cmd) = make_cmd(CMD_SLEEP); + (yyval.cmd)->timeoutSeconds = (yyvsp[(2) - (2)].ival); break; + } case 153: #line 1181 "test_spec_parse.y" - { - (yyval.cmd) = make_cmd(CMD_COMPOSE_DOWN); - } + { + (yyval.cmd) = make_cmd(CMD_COMPOSE_DOWN); break; + } case 154: #line 1185 "test_spec_parse.y" - { - (yyval.cmd) = make_cmd(CMD_COMPOSE_START); - strlcpy((yyval.cmd)->service, (yyvsp[(3) - (3)].str), - sizeof((yyval.cmd)->service)); - free((yyvsp[(3) - (3)].str)); - } + { + (yyval.cmd) = make_cmd(CMD_COMPOSE_START); + strlcpy((yyval.cmd)->service, (yyvsp[(3) - (3)].str), + sizeof((yyval.cmd)->service)); + free((yyvsp[(3) - (3)].str)); break; + } case 155: #line 1191 "test_spec_parse.y" - { - (yyval.cmd) = make_cmd(CMD_COMPOSE_STOP); - strlcpy((yyval.cmd)->service, (yyvsp[(3) - (3)].str), - sizeof((yyval.cmd)->service)); - free((yyvsp[(3) - (3)].str)); - } + { + (yyval.cmd) = make_cmd(CMD_COMPOSE_STOP); + strlcpy((yyval.cmd)->service, (yyvsp[(3) - (3)].str), + sizeof((yyval.cmd)->service)); + free((yyvsp[(3) - (3)].str)); break; + } case 156: #line 1197 "test_spec_parse.y" - { - (yyval.cmd) = make_cmd(CMD_COMPOSE_KILL); - strlcpy((yyval.cmd)->service, (yyvsp[(3) - (3)].str), - sizeof((yyval.cmd)->service)); - free((yyvsp[(3) - (3)].str)); - } + { + (yyval.cmd) = make_cmd(CMD_COMPOSE_KILL); + strlcpy((yyval.cmd)->service, (yyvsp[(3) - (3)].str), + sizeof((yyval.cmd)->service)); + free((yyvsp[(3) - (3)].str)); break; + } case 157: #line 1223 "test_spec_parse.y" + { + (yyval.cmd) = make_cmd(CMD_COMPOSE_INJECT); + strlcpy((yyval.cmd)->expected, (yyvsp[(3) - (4)].str), + sizeof((yyval.cmd)->expected)); /* image */ + + /* Split T_SHELL_ARGS: " :" */ + char tmp[4096]; + strlcpy(tmp, (yyvsp[(4) - (4)].str), sizeof(tmp)); + char *src = tmp; + char *p = tmp; + while (*p && *p != ' ' && *p != '\t') { - (yyval.cmd) = make_cmd(CMD_COMPOSE_INJECT); - strlcpy((yyval.cmd)->expected, (yyvsp[(3) - (4)].str), - sizeof((yyval.cmd)->expected)); /* image */ - - /* Split T_SHELL_ARGS: " :" */ - char tmp[4096]; - strlcpy(tmp, (yyvsp[(4) - (4)].str), sizeof(tmp)); - char *src = tmp; - char *p = tmp; - while (*p && *p != ' ' && *p != '\t') + p++; + } + if (*p) + { + *p++ = '\0'; + while (*p == ' ' || *p == '\t') { p++; } - if (*p) - { - *p++ = '\0'; - while (*p == ' ' || *p == '\t') - { - p++; - } - } - char *svcdst = p; - char *colon = (*svcdst) ? strchr(svcdst, ':') : NULL; - strlcpy((yyval.cmd)->args, src, sizeof((yyval.cmd)->args)); - if (colon) - { - *colon = '\0'; - strlcpy((yyval.cmd)->service, svcdst, sizeof((yyval.cmd)->service)); /* dst svc */ - strlcpy((yyval.cmd)->state, colon + 1, sizeof((yyval.cmd)->state)); /* dst path */ - } - free((yyvsp[(3) - (4)].str)); - free((yyvsp[(4) - (4)].str)); } + char *svcdst = p; + char *colon = (*svcdst) ? strchr(svcdst, ':') : NULL; + strlcpy((yyval.cmd)->args, src, sizeof((yyval.cmd)->args)); + if (colon) + { + *colon = '\0'; + strlcpy((yyval.cmd)->service, svcdst, sizeof((yyval.cmd)->service)); /* dst svc */ + strlcpy((yyval.cmd)->state, colon + 1, sizeof((yyval.cmd)->state)); /* dst path */ + } + free((yyvsp[(3) - (4)].str)); + free((yyvsp[(4) - (4)].str)); break; + } case 158: #line 1257 "test_spec_parse.y" - { - (yyval.cmd) = make_cmd(CMD_STOP_POSTGRES); - strlcpy((yyval.cmd)->service, (yyvsp[(3) - (3)].str), - sizeof((yyval.cmd)->service)); - free((yyvsp[(3) - (3)].str)); - } + { + (yyval.cmd) = make_cmd(CMD_STOP_POSTGRES); + strlcpy((yyval.cmd)->service, (yyvsp[(3) - (3)].str), + sizeof((yyval.cmd)->service)); + free((yyvsp[(3) - (3)].str)); break; + } case 159: #line 1263 "test_spec_parse.y" - { - (yyval.cmd) = make_cmd(CMD_START_POSTGRES); - strlcpy((yyval.cmd)->service, (yyvsp[(3) - (3)].str), - sizeof((yyval.cmd)->service)); - free((yyvsp[(3) - (3)].str)); - } + { + (yyval.cmd) = make_cmd(CMD_START_POSTGRES); + strlcpy((yyval.cmd)->service, (yyvsp[(3) - (3)].str), + sizeof((yyval.cmd)->service)); + free((yyvsp[(3) - (3)].str)); break; + } case 160: #line 1279 "test_spec_parse.y" - { - pgaf_next_brace_is_while = 1; - } + { + pgaf_next_brace_is_while = 1; break; + } case 161: #line 1280 "test_spec_parse.y" - { - (yyval.step) = (yyvsp[(4) - (5)].step); - } + { + (yyval.step) = (yyvsp[(4) - (5)].step); break; + } case 162: #line 1285 "test_spec_parse.y" - { - (yyval.cmd) = make_cmd(CMD_STAYS_WHILE); - strlcpy((yyval.cmd)->service, (yyvsp[(2) - (5)].str), - sizeof((yyval.cmd)->service)); - strlcpy((yyval.cmd)->state, (yyvsp[(4) - (5)].str), - sizeof((yyval.cmd)->state)); - (yyval.cmd)->body = ((yyvsp[(5) - (5)].step)) ? (yyvsp[(5) - - (5)].step)-> - commands : NULL; - free((yyvsp[(2) - (5)].str)); - } + { + (yyval.cmd) = make_cmd(CMD_STAYS_WHILE); + strlcpy((yyval.cmd)->service, (yyvsp[(2) - (5)].str), + sizeof((yyval.cmd)->service)); + strlcpy((yyval.cmd)->state, (yyvsp[(4) - (5)].str), + sizeof((yyval.cmd)->state)); + (yyval.cmd)->body = ((yyvsp[(5) - (5)].step)) ? (yyvsp[(5) - + (5)].step)-> + commands : NULL; + free((yyvsp[(2) - (5)].str)); break; + } case 163: #line 1304 "test_spec_parse.y" - { - /* only "set monitor " is supported; $2 must be "monitor" */ - if (strcmp((yyvsp[(2) - (3)].str), "monitor") != 0) - { - fprintf(/* IGNORE-BANNED */ stderr, - "pgaftest: unknown 'set' target '%s' (expected 'monitor')\n", - (yyvsp[(2) - - ( - 3)].str)); - free((yyvsp[(2) - (3)].str)); - free((yyvsp[(3) - (3)].str)); - YYERROR; - } - (yyval.cmd) = make_cmd(CMD_SET_MONITOR); - strlcpy((yyval.cmd)->service, (yyvsp[(3) - (3)].str), - sizeof((yyval.cmd)->service)); + { + /* only "set monitor " is supported; $2 must be "monitor" */ + if (strcmp((yyvsp[(2) - (3)].str), "monitor") != 0) + { + fprintf(/* IGNORE-BANNED */ stderr, + "pgaftest: unknown 'set' target '%s' (expected 'monitor')\n", + (yyvsp[(2) - + ( + 3)].str)); free((yyvsp[(2) - (3)].str)); free((yyvsp[(3) - (3)].str)); + YYERROR; } + (yyval.cmd) = make_cmd(CMD_SET_MONITOR); + strlcpy((yyval.cmd)->service, (yyvsp[(3) - (3)].str), + sizeof((yyval.cmd)->service)); + free((yyvsp[(2) - (3)].str)); + free((yyvsp[(3) - (3)].str)); break; + } case 164: #line 1329 "test_spec_parse.y" - { - (yyval.cmd) = make_cmd(CMD_LOGS_CHECK); - strlcpy((yyval.cmd)->service, (yyvsp[(2) - (4)].str), - sizeof((yyval.cmd)->service)); - strlcpy((yyval.cmd)->args, (yyvsp[(4) - (4)].str), - sizeof((yyval.cmd)->args)); - (yyval.cmd)->logsNegate = false; - (yyval.cmd)->allowError = false; /* false = fixed string, true = PCRE */ - free((yyvsp[(2) - (4)].str)); - free((yyvsp[(4) - (4)].str)); - } + { + (yyval.cmd) = make_cmd(CMD_LOGS_CHECK); + strlcpy((yyval.cmd)->service, (yyvsp[(2) - (4)].str), + sizeof((yyval.cmd)->service)); + strlcpy((yyval.cmd)->args, (yyvsp[(4) - (4)].str), + sizeof((yyval.cmd)->args)); + (yyval.cmd)->logsNegate = false; + (yyval.cmd)->allowError = false; /* false = fixed string, true = PCRE */ + free((yyvsp[(2) - (4)].str)); + free((yyvsp[(4) - (4)].str)); break; + } case 165: #line 1338 "test_spec_parse.y" - { - (yyval.cmd) = make_cmd(CMD_LOGS_CHECK); - strlcpy((yyval.cmd)->service, (yyvsp[(2) - (5)].str), - sizeof((yyval.cmd)->service)); - strlcpy((yyval.cmd)->args, (yyvsp[(5) - (5)].str), - sizeof((yyval.cmd)->args)); - (yyval.cmd)->logsNegate = true; - (yyval.cmd)->allowError = false; - free((yyvsp[(2) - (5)].str)); - free((yyvsp[(5) - (5)].str)); - } + { + (yyval.cmd) = make_cmd(CMD_LOGS_CHECK); + strlcpy((yyval.cmd)->service, (yyvsp[(2) - (5)].str), + sizeof((yyval.cmd)->service)); + strlcpy((yyval.cmd)->args, (yyvsp[(5) - (5)].str), + sizeof((yyval.cmd)->args)); + (yyval.cmd)->logsNegate = true; + (yyval.cmd)->allowError = false; + free((yyvsp[(2) - (5)].str)); + free((yyvsp[(5) - (5)].str)); break; + } case 166: #line 1347 "test_spec_parse.y" - { - (yyval.cmd) = make_cmd(CMD_LOGS_CHECK); - strlcpy((yyval.cmd)->service, (yyvsp[(2) - (4)].str), - sizeof((yyval.cmd)->service)); - strlcpy((yyval.cmd)->args, (yyvsp[(4) - (4)].str), - sizeof((yyval.cmd)->args)); - (yyval.cmd)->logsNegate = false; - (yyval.cmd)->allowError = true; /* true = PCRE (-P) */ - free((yyvsp[(2) - (4)].str)); - free((yyvsp[(4) - (4)].str)); - } + { + (yyval.cmd) = make_cmd(CMD_LOGS_CHECK); + strlcpy((yyval.cmd)->service, (yyvsp[(2) - (4)].str), + sizeof((yyval.cmd)->service)); + strlcpy((yyval.cmd)->args, (yyvsp[(4) - (4)].str), + sizeof((yyval.cmd)->args)); + (yyval.cmd)->logsNegate = false; + (yyval.cmd)->allowError = true; /* true = PCRE (-P) */ + free((yyvsp[(2) - (4)].str)); + free((yyvsp[(4) - (4)].str)); break; + } case 167: #line 1356 "test_spec_parse.y" - { - (yyval.cmd) = make_cmd(CMD_LOGS_CHECK); - strlcpy((yyval.cmd)->service, (yyvsp[(2) - (5)].str), - sizeof((yyval.cmd)->service)); - strlcpy((yyval.cmd)->args, (yyvsp[(5) - (5)].str), - sizeof((yyval.cmd)->args)); - (yyval.cmd)->logsNegate = true; - (yyval.cmd)->allowError = true; - free((yyvsp[(2) - (5)].str)); - free((yyvsp[(5) - (5)].str)); - } + { + (yyval.cmd) = make_cmd(CMD_LOGS_CHECK); + strlcpy((yyval.cmd)->service, (yyvsp[(2) - (5)].str), + sizeof((yyval.cmd)->service)); + strlcpy((yyval.cmd)->args, (yyvsp[(5) - (5)].str), + sizeof((yyval.cmd)->args)); + (yyval.cmd)->logsNegate = true; + (yyval.cmd)->allowError = true; + free((yyvsp[(2) - (5)].str)); + free((yyvsp[(5) - (5)].str)); break; + } case 170: #line 1377 "test_spec_parse.y" + { + int i = current_spec->sequenceLength; + if (i < PGAF_MAX_SEQ) { - int i = current_spec->sequenceLength; - if (i < PGAF_MAX_SEQ) - { - current_spec->sequence[current_spec->sequenceLength++] = (yyvsp[(2) - - (2)]. - str); - } - else - { - fprintf(/* IGNORE-BANNED */ stderr, - "pgaftest: too many steps in sequence (max %d)\n", - PGAF_MAX_SEQ); - exit(1); - } + current_spec->sequence[current_spec->sequenceLength++] = (yyvsp[(2) - + (2)]. + str); + } + else + { + fprintf(/* IGNORE-BANNED */ stderr, + "pgaftest: too many steps in sequence (max %d)\n", + PGAF_MAX_SEQ); + exit(1); } break; + } case 171: #line 1398 "test_spec_parse.y" - { - (yyval.str) = "init"; - } + { + (yyval.str) = "init"; break; + } case 172: #line 1399 "test_spec_parse.y" - { - (yyval.str) = "single"; - } + { + (yyval.str) = "single"; break; + } case 173: #line 1400 "test_spec_parse.y" - { - (yyval.str) = "primary"; - } + { + (yyval.str) = "primary"; break; + } case 174: #line 1401 "test_spec_parse.y" - { - (yyval.str) = "wait_primary"; - } + { + (yyval.str) = "wait_primary"; break; + } case 175: #line 1402 "test_spec_parse.y" - { - (yyval.str) = "wait_standby"; - } + { + (yyval.str) = "wait_standby"; break; + } case 176: #line 1403 "test_spec_parse.y" - { - (yyval.str) = "demoted"; - } + { + (yyval.str) = "demoted"; break; + } case 177: #line 1404 "test_spec_parse.y" - { - (yyval.str) = "demote_timeout"; - } + { + (yyval.str) = "demote_timeout"; break; + } case 178: #line 1405 "test_spec_parse.y" - { - (yyval.str) = "draining"; - } + { + (yyval.str) = "draining"; break; + } case 179: #line 1406 "test_spec_parse.y" - { - (yyval.str) = "secondary"; - } + { + (yyval.str) = "secondary"; break; + } case 180: #line 1407 "test_spec_parse.y" - { - (yyval.str) = "catchingup"; - } + { + (yyval.str) = "catchingup"; break; + } case 181: #line 1408 "test_spec_parse.y" - { - (yyval.str) = "prepare_promotion"; - } + { + (yyval.str) = "prepare_promotion"; break; + } case 182: #line 1409 "test_spec_parse.y" - { - (yyval.str) = "stop_replication"; - } + { + (yyval.str) = "stop_replication"; break; + } case 183: #line 1410 "test_spec_parse.y" - { - (yyval.str) = "maintenance"; - } + { + (yyval.str) = "maintenance"; break; + } case 184: #line 1411 "test_spec_parse.y" - { - (yyval.str) = "join_primary"; - } + { + (yyval.str) = "join_primary"; break; + } case 185: #line 1412 "test_spec_parse.y" - { - (yyval.str) = "apply_settings"; - } + { + (yyval.str) = "apply_settings"; break; + } case 186: #line 1413 "test_spec_parse.y" - { - (yyval.str) = "prepare_maintenance"; - } + { + (yyval.str) = "prepare_maintenance"; break; + } case 187: #line 1414 "test_spec_parse.y" - { - (yyval.str) = "wait_maintenance"; - } + { + (yyval.str) = "wait_maintenance"; break; + } case 188: #line 1415 "test_spec_parse.y" - { - (yyval.str) = "report_lsn"; - } + { + (yyval.str) = "report_lsn"; break; + } case 189: #line 1416 "test_spec_parse.y" - { - (yyval.str) = "fast_forward"; - } + { + (yyval.str) = "fast_forward"; break; + } case 190: #line 1417 "test_spec_parse.y" - { - (yyval.str) = "join_secondary"; - } + { + (yyval.str) = "join_secondary"; break; + } case 191: #line 1418 "test_spec_parse.y" - { - (yyval.str) = "dropped"; - } + { + (yyval.str) = "dropped"; break; + } case 192: #line 1426 "test_spec_parse.y" - { - (yyval.str) = (yyvsp[(1) - (1)].str); - } + { + (yyval.str) = (yyvsp[(1) - (1)].str); break; + } case 193: #line 1427 "test_spec_parse.y" - { - (yyval.str) = (yyvsp[(1) - (1)].str); - } + { + (yyval.str) = (yyvsp[(1) - (1)].str); break; + } /* Line 1267 of yacc.c. */ @@ -4009,8 +4011,8 @@ parse_test_spec(const char *filename) if (!f) { fprintf(/* IGNORE-BANNED */ stderr, - "pgaftest: cannot open spec file \"%s\": %s\n", - filename, strerror(errno) /* IGNORE-BANNED */); + "pgaftest: cannot open spec file \"%s\": %s\n", + filename, strerror(errno) /* IGNORE-BANNED */); return NULL; } diff --git a/src/bin/pgaftest/test_spec_scan.c b/src/bin/pgaftest/test_spec_scan.c index ecadc3603..c51cde914 100644 --- a/src/bin/pgaftest/test_spec_scan.c +++ b/src/bin/pgaftest/test_spec_scan.c @@ -173,17 +173,17 @@ extern FILE *yyin, *yyout; /* Return all but the first "n" matched characters back to the input stream. */ #define yyless(n) \ - do \ - { \ - /* Undo effects of setting up yytext. */ \ - int yyless_macro_arg = (n); \ - YY_LESS_LINENO(yyless_macro_arg); \ - *yy_cp = (yy_hold_char); \ - YY_RESTORE_YY_MORE_OFFSET \ - (yy_c_buf_p) = yy_cp = yy_bp + yyless_macro_arg - YY_MORE_ADJ; \ - YY_DO_BEFORE_ACTION; /* set up yytext again */ \ - } \ - while (0) + do \ + { \ + /* Undo effects of setting up yytext. */ \ + int yyless_macro_arg = (n); \ + YY_LESS_LINENO(yyless_macro_arg); \ + *yy_cp = (yy_hold_char); \ + YY_RESTORE_YY_MORE_OFFSET \ + (yy_c_buf_p) = yy_cp = yy_bp + yyless_macro_arg - YY_MORE_ADJ; \ + YY_DO_BEFORE_ACTION; /* set up yytext again */ \ + } \ + while (0) #define unput(c) yyunput(c, (yytext_ptr)) #ifndef YY_STRUCT_YY_BUFFER_STATE @@ -309,23 +309,23 @@ void yyfree(void *); #define yy_new_buffer yy_create_buffer #define yy_set_interactive(is_interactive) \ - { \ - if (!YY_CURRENT_BUFFER) { \ - yyensure_buffer_stack(); \ - YY_CURRENT_BUFFER_LVALUE = \ - yy_create_buffer(yyin, YY_BUF_SIZE); \ - } \ - YY_CURRENT_BUFFER_LVALUE->yy_is_interactive = is_interactive; \ - } + { \ + if (!YY_CURRENT_BUFFER) { \ + yyensure_buffer_stack(); \ + YY_CURRENT_BUFFER_LVALUE = \ + yy_create_buffer(yyin, YY_BUF_SIZE); \ + } \ + YY_CURRENT_BUFFER_LVALUE->yy_is_interactive = is_interactive; \ + } #define yy_set_bol(at_bol) \ - { \ - if (!YY_CURRENT_BUFFER) { \ - yyensure_buffer_stack(); \ - YY_CURRENT_BUFFER_LVALUE = \ - yy_create_buffer(yyin, YY_BUF_SIZE); \ - } \ - YY_CURRENT_BUFFER_LVALUE->yy_at_bol = at_bol; \ - } + { \ + if (!YY_CURRENT_BUFFER) { \ + yyensure_buffer_stack(); \ + YY_CURRENT_BUFFER_LVALUE = \ + yy_create_buffer(yyin, YY_BUF_SIZE); \ + } \ + YY_CURRENT_BUFFER_LVALUE->yy_at_bol = at_bol; \ + } #define YY_AT_BOL() (YY_CURRENT_BUFFER_LVALUE->yy_at_bol) /* Begin user sect3 */ @@ -356,11 +356,11 @@ static void yynoreturn yy_fatal_error(const char *msg); * corresponding action - sets up yytext. */ #define YY_DO_BEFORE_ACTION \ - (yytext_ptr) = yy_bp; \ - yyleng = (yy_size_t) (yy_cp - yy_bp); \ - (yy_hold_char) = *yy_cp; \ - *yy_cp = '\0'; \ - (yy_c_buf_p) = yy_cp; + (yytext_ptr) = yy_bp; \ + yyleng = (yy_size_t) (yy_cp - yy_bp); \ + (yy_hold_char) = *yy_cp; \ + *yy_cp = '\0'; \ + (yy_c_buf_p) = yy_cp; #define YY_NUM_RULES 147 #define YY_END_OF_BUFFER 148 @@ -1320,34 +1320,35 @@ static int input(void); */ #ifndef YY_INPUT #define YY_INPUT(buf, result, max_size) \ - if (YY_CURRENT_BUFFER_LVALUE->yy_is_interactive) \ - { \ - int c = '*'; \ - yy_size_t n; \ - for (n = 0; n < max_size && \ - (c = getc(yyin)) != EOF && c != '\n'; ++n) { \ - buf[n] = (char) c; } \ - if (c == '\n') { \ - buf[n++] = (char) c; } \ - if (c == EOF && ferror(yyin)) { \ - YY_FATAL_ERROR("input in flex scanner failed"); } \ - result = n; \ - } \ - else \ - { \ - errno = 0; \ - while ((result = (int) fread(buf, 1, (yy_size_t) max_size, yyin)) == 0 && ferror( \ - yyin)) \ + if (YY_CURRENT_BUFFER_LVALUE->yy_is_interactive) \ { \ - if (errno != EINTR) \ + int c = '*'; \ + yy_size_t n; \ + for (n = 0; n < max_size && \ + (c = getc(yyin)) != EOF && c != '\n'; ++n) { \ + buf[n] = (char) c; } \ + if (c == '\n') { \ + buf[n++] = (char) c; } \ + if (c == EOF && ferror(yyin)) { \ + YY_FATAL_ERROR("input in flex scanner failed"); } \ + result = n; \ + } \ + else \ + { \ + errno = 0; \ + while ((result = (int) fread(buf, 1, (yy_size_t) max_size, yyin)) == 0 && \ + ferror( \ + yyin)) \ { \ - YY_FATAL_ERROR("input in flex scanner failed"); \ - break; \ + if (errno != EINTR) \ + { \ + YY_FATAL_ERROR("input in flex scanner failed"); \ + break; \ + } \ + errno = 0; \ + clearerr(yyin); \ } \ - errno = 0; \ - clearerr(yyin); \ } \ - } \ \ #endif @@ -1396,11 +1397,11 @@ extern int yylex(void); #endif #define YY_RULE_SETUP \ - YY_USER_ACTION + YY_USER_ACTION /** The main scanner function which does all the work. */ -YY_DECL + YY_DECL { yy_state_type yy_current_state; char *yy_cp, *yy_bp; @@ -1490,19 +1491,25 @@ YY_DECL { /* beginning of action switch */ case 0: /* must back up */ /* undo the effects of YY_DO_BEFORE_ACTION */ + { *yy_cp = (yy_hold_char); yy_cp = (yy_last_accepting_cpos); yy_current_state = (yy_last_accepting_state); goto yy_find_action; + } case 1: + { YY_RULE_SETUP #line 91 "test_spec_scan.l" { /* comment */ } YY_BREAK + } + case 2: + { YY_RULE_SETUP #line 93 "test_spec_scan.l" { @@ -1511,7 +1518,10 @@ YY_DECL } YY_BREAK + } + case 3: + { YY_RULE_SETUP #line 94 "test_spec_scan.l" { @@ -1519,7 +1529,10 @@ YY_DECL } YY_BREAK + } + case 4: + { YY_RULE_SETUP #line 95 "test_spec_scan.l" { @@ -1527,7 +1540,10 @@ YY_DECL } YY_BREAK + } + case 5: + { YY_RULE_SETUP #line 96 "test_spec_scan.l" { @@ -1535,7 +1551,10 @@ YY_DECL } YY_BREAK + } + case 6: + { YY_RULE_SETUP #line 97 "test_spec_scan.l" { @@ -1543,7 +1562,10 @@ YY_DECL } YY_BREAK + } + case 7: + { YY_RULE_SETUP #line 99 "test_spec_scan.l" { @@ -1552,7 +1574,10 @@ YY_DECL } YY_BREAK + } + case 8: + { YY_RULE_SETUP #line 100 "test_spec_scan.l" { @@ -1561,7 +1586,10 @@ YY_DECL } YY_BREAK + } + case 9: + { YY_RULE_SETUP #line 101 "test_spec_scan.l" { @@ -1570,7 +1598,10 @@ YY_DECL } YY_BREAK + } + case 10: + { YY_RULE_SETUP #line 102 "test_spec_scan.l" { @@ -1578,7 +1609,10 @@ YY_DECL } YY_BREAK + } + case 11: + { YY_RULE_SETUP #line 104 "test_spec_scan.l" { @@ -1586,7 +1620,10 @@ YY_DECL } YY_BREAK + } + case 12: + { YY_RULE_SETUP #line 106 "test_spec_scan.l" { @@ -1595,7 +1632,10 @@ YY_DECL } YY_BREAK + } + case 13: + { YY_RULE_SETUP #line 111 "test_spec_scan.l" { @@ -1604,8 +1644,10 @@ YY_DECL } YY_BREAK - case 14: + } + case 14: + { /* rule 14 can match eol */ YY_RULE_SETUP #line 116 "test_spec_scan.l" @@ -1616,7 +1658,10 @@ YY_DECL } YY_BREAK + } + case 15: + { YY_RULE_SETUP #line 122 "test_spec_scan.l" { @@ -1636,13 +1681,15 @@ YY_DECL /* fallback: should not happen in a well-formed file */ fprintf(/* IGNORE-BANNED */ stderr, - "pgaftest: unexpected '{' at line %d\n", - pgaf_line_number); + "pgaftest: unexpected '{' at line %d\n", + pgaf_line_number); } YY_BREAK - case 16: + } + case 16: + { /* rule 16 can match eol */ YY_RULE_SETUP #line 140 "test_spec_scan.l" @@ -1651,32 +1698,43 @@ YY_DECL } YY_BREAK + } + case 17: + { YY_RULE_SETUP #line 141 "test_spec_scan.l" { /* whitespace */ } YY_BREAK + } + case 18: + { YY_RULE_SETUP #line 143 "test_spec_scan.l" { fprintf(/* IGNORE-BANNED */ stderr, - "pgaftest: unexpected character '%c' at line %d\n", - yytext[0], pgaf_line_number); + "pgaftest: unexpected character '%c' at line %d\n", + yytext[0], pgaf_line_number); } YY_BREAK + } + case 19: + { YY_RULE_SETUP #line 148 "test_spec_scan.l" { /* comment */ } YY_BREAK - case 20: + } + case 20: + { /* rule 20 can match eol */ YY_RULE_SETUP #line 149 "test_spec_scan.l" @@ -1685,14 +1743,20 @@ YY_DECL } YY_BREAK + } + case 21: + { YY_RULE_SETUP #line 150 "test_spec_scan.l" { /* whitespace */ } YY_BREAK + } + case 22: + { YY_RULE_SETUP #line 152 "test_spec_scan.l" { @@ -1700,7 +1764,10 @@ YY_DECL } YY_BREAK + } + case 23: + { YY_RULE_SETUP #line 153 "test_spec_scan.l" { @@ -1708,7 +1775,10 @@ YY_DECL } YY_BREAK + } + case 24: + { YY_RULE_SETUP #line 154 "test_spec_scan.l" { @@ -1716,7 +1786,10 @@ YY_DECL } YY_BREAK + } + case 25: + { YY_RULE_SETUP #line 155 "test_spec_scan.l" { @@ -1724,7 +1797,10 @@ YY_DECL } YY_BREAK + } + case 26: + { YY_RULE_SETUP #line 156 "test_spec_scan.l" { @@ -1732,7 +1808,10 @@ YY_DECL } YY_BREAK + } + case 27: + { YY_RULE_SETUP #line 157 "test_spec_scan.l" { @@ -1740,7 +1819,10 @@ YY_DECL } YY_BREAK + } + case 28: + { YY_RULE_SETUP #line 158 "test_spec_scan.l" { @@ -1748,7 +1830,10 @@ YY_DECL } YY_BREAK + } + case 29: + { YY_RULE_SETUP #line 159 "test_spec_scan.l" { @@ -1756,7 +1841,10 @@ YY_DECL } YY_BREAK + } + case 30: + { YY_RULE_SETUP #line 160 "test_spec_scan.l" { @@ -1764,7 +1852,10 @@ YY_DECL } YY_BREAK + } + case 31: + { YY_RULE_SETUP #line 162 "test_spec_scan.l" { @@ -1772,7 +1863,10 @@ YY_DECL } YY_BREAK + } + case 32: + { YY_RULE_SETUP #line 163 "test_spec_scan.l" { @@ -1780,7 +1874,10 @@ YY_DECL } YY_BREAK + } + case 33: + { YY_RULE_SETUP #line 164 "test_spec_scan.l" { @@ -1788,7 +1885,10 @@ YY_DECL } YY_BREAK + } + case 34: + { YY_RULE_SETUP #line 165 "test_spec_scan.l" { @@ -1796,7 +1896,10 @@ YY_DECL } YY_BREAK + } + case 35: + { YY_RULE_SETUP #line 166 "test_spec_scan.l" { @@ -1804,7 +1907,10 @@ YY_DECL } YY_BREAK + } + case 36: + { YY_RULE_SETUP #line 167 "test_spec_scan.l" { @@ -1812,7 +1918,10 @@ YY_DECL } YY_BREAK + } + case 37: + { YY_RULE_SETUP #line 168 "test_spec_scan.l" { @@ -1820,7 +1929,10 @@ YY_DECL } YY_BREAK + } + case 38: + { YY_RULE_SETUP #line 169 "test_spec_scan.l" { @@ -1828,7 +1940,10 @@ YY_DECL } YY_BREAK + } + case 39: + { YY_RULE_SETUP #line 170 "test_spec_scan.l" { @@ -1836,7 +1951,10 @@ YY_DECL } YY_BREAK + } + case 40: + { YY_RULE_SETUP #line 171 "test_spec_scan.l" { @@ -1844,7 +1962,10 @@ YY_DECL } YY_BREAK + } + case 41: + { YY_RULE_SETUP #line 172 "test_spec_scan.l" { @@ -1852,7 +1973,10 @@ YY_DECL } YY_BREAK + } + case 42: + { YY_RULE_SETUP #line 173 "test_spec_scan.l" { @@ -1860,7 +1984,10 @@ YY_DECL } YY_BREAK + } + case 43: + { YY_RULE_SETUP #line 174 "test_spec_scan.l" { @@ -1868,7 +1995,10 @@ YY_DECL } YY_BREAK + } + case 44: + { YY_RULE_SETUP #line 175 "test_spec_scan.l" { @@ -1876,7 +2006,10 @@ YY_DECL } YY_BREAK + } + case 45: + { YY_RULE_SETUP #line 176 "test_spec_scan.l" { @@ -1884,7 +2017,10 @@ YY_DECL } YY_BREAK + } + case 46: + { YY_RULE_SETUP #line 177 "test_spec_scan.l" { @@ -1892,7 +2028,10 @@ YY_DECL } YY_BREAK + } + case 47: + { YY_RULE_SETUP #line 178 "test_spec_scan.l" { @@ -1900,7 +2039,10 @@ YY_DECL } YY_BREAK + } + case 48: + { YY_RULE_SETUP #line 179 "test_spec_scan.l" { @@ -1908,7 +2050,10 @@ YY_DECL } YY_BREAK + } + case 49: + { YY_RULE_SETUP #line 180 "test_spec_scan.l" { @@ -1916,7 +2061,10 @@ YY_DECL } YY_BREAK + } + case 50: + { YY_RULE_SETUP #line 181 "test_spec_scan.l" { @@ -1924,7 +2072,10 @@ YY_DECL } YY_BREAK + } + case 51: + { YY_RULE_SETUP #line 182 "test_spec_scan.l" { @@ -1932,7 +2083,10 @@ YY_DECL } YY_BREAK + } + case 52: + { YY_RULE_SETUP #line 183 "test_spec_scan.l" { @@ -1940,7 +2094,10 @@ YY_DECL } YY_BREAK + } + case 53: + { YY_RULE_SETUP #line 184 "test_spec_scan.l" { @@ -1948,7 +2105,10 @@ YY_DECL } YY_BREAK + } + case 54: + { YY_RULE_SETUP #line 186 "test_spec_scan.l" { @@ -1956,7 +2116,10 @@ YY_DECL } YY_BREAK + } + case 55: + { YY_RULE_SETUP #line 188 "test_spec_scan.l" { @@ -1965,8 +2128,10 @@ YY_DECL } YY_BREAK - case 56: + } + case 56: + { /* rule 56 can match eol */ YY_RULE_SETUP #line 193 "test_spec_scan.l" @@ -1977,7 +2142,10 @@ YY_DECL } YY_BREAK + } + case 57: + { YY_RULE_SETUP #line 199 "test_spec_scan.l" { @@ -1986,7 +2154,10 @@ YY_DECL } YY_BREAK + } + case 58: + { YY_RULE_SETUP #line 204 "test_spec_scan.l" { @@ -1999,7 +2170,10 @@ YY_DECL } YY_BREAK + } + case 59: + { YY_RULE_SETUP #line 211 "test_spec_scan.l" { @@ -2007,7 +2181,10 @@ YY_DECL } YY_BREAK + } + case 60: + { YY_RULE_SETUP #line 212 "test_spec_scan.l" { @@ -2015,7 +2192,10 @@ YY_DECL } YY_BREAK + } + case 61: + { YY_RULE_SETUP #line 213 "test_spec_scan.l" { @@ -2023,7 +2203,10 @@ YY_DECL } YY_BREAK + } + case 62: + { YY_RULE_SETUP #line 214 "test_spec_scan.l" { @@ -2031,7 +2214,10 @@ YY_DECL } YY_BREAK + } + case 63: + { YY_RULE_SETUP #line 215 "test_spec_scan.l" { @@ -2039,7 +2225,10 @@ YY_DECL } YY_BREAK + } + case 64: + { YY_RULE_SETUP #line 216 "test_spec_scan.l" { @@ -2047,7 +2236,10 @@ YY_DECL } YY_BREAK + } + case 65: + { YY_RULE_SETUP #line 217 "test_spec_scan.l" { @@ -2055,7 +2247,10 @@ YY_DECL } YY_BREAK + } + case 66: + { YY_RULE_SETUP #line 218 "test_spec_scan.l" { @@ -2063,7 +2258,10 @@ YY_DECL } YY_BREAK + } + case 67: + { YY_RULE_SETUP #line 219 "test_spec_scan.l" { @@ -2071,7 +2269,10 @@ YY_DECL } YY_BREAK + } + case 68: + { YY_RULE_SETUP #line 220 "test_spec_scan.l" { @@ -2079,7 +2280,10 @@ YY_DECL } YY_BREAK + } + case 69: + { YY_RULE_SETUP #line 221 "test_spec_scan.l" { @@ -2087,7 +2291,10 @@ YY_DECL } YY_BREAK + } + case 70: + { YY_RULE_SETUP #line 222 "test_spec_scan.l" { @@ -2095,7 +2302,10 @@ YY_DECL } YY_BREAK + } + case 71: + { YY_RULE_SETUP #line 223 "test_spec_scan.l" { @@ -2103,7 +2313,10 @@ YY_DECL } YY_BREAK + } + case 72: + { YY_RULE_SETUP #line 224 "test_spec_scan.l" { @@ -2111,7 +2324,10 @@ YY_DECL } YY_BREAK + } + case 73: + { YY_RULE_SETUP #line 225 "test_spec_scan.l" { @@ -2119,7 +2335,10 @@ YY_DECL } YY_BREAK + } + case 74: + { YY_RULE_SETUP #line 226 "test_spec_scan.l" { @@ -2127,7 +2346,10 @@ YY_DECL } YY_BREAK + } + case 75: + { YY_RULE_SETUP #line 227 "test_spec_scan.l" { @@ -2135,7 +2357,10 @@ YY_DECL } YY_BREAK + } + case 76: + { YY_RULE_SETUP #line 228 "test_spec_scan.l" { @@ -2143,7 +2368,10 @@ YY_DECL } YY_BREAK + } + case 77: + { YY_RULE_SETUP #line 229 "test_spec_scan.l" { @@ -2151,7 +2379,10 @@ YY_DECL } YY_BREAK + } + case 78: + { YY_RULE_SETUP #line 230 "test_spec_scan.l" { @@ -2159,7 +2390,10 @@ YY_DECL } YY_BREAK + } + case 79: + { YY_RULE_SETUP #line 231 "test_spec_scan.l" { @@ -2167,7 +2401,10 @@ YY_DECL } YY_BREAK + } + case 80: + { YY_RULE_SETUP #line 232 "test_spec_scan.l" { @@ -2175,7 +2412,10 @@ YY_DECL } YY_BREAK + } + case 81: + { YY_RULE_SETUP #line 233 "test_spec_scan.l" { @@ -2183,7 +2423,10 @@ YY_DECL } YY_BREAK + } + case 82: + { YY_RULE_SETUP #line 234 "test_spec_scan.l" { @@ -2191,7 +2434,10 @@ YY_DECL } YY_BREAK + } + case 83: + { YY_RULE_SETUP #line 235 "test_spec_scan.l" { @@ -2199,7 +2445,10 @@ YY_DECL } YY_BREAK + } + case 84: + { YY_RULE_SETUP #line 236 "test_spec_scan.l" { @@ -2207,7 +2456,10 @@ YY_DECL } YY_BREAK + } + case 85: + { YY_RULE_SETUP #line 237 "test_spec_scan.l" { @@ -2215,7 +2467,10 @@ YY_DECL } YY_BREAK + } + case 86: + { YY_RULE_SETUP #line 238 "test_spec_scan.l" { @@ -2223,7 +2478,10 @@ YY_DECL } YY_BREAK + } + case 87: + { YY_RULE_SETUP #line 239 "test_spec_scan.l" { @@ -2231,7 +2489,10 @@ YY_DECL } YY_BREAK + } + case 88: + { YY_RULE_SETUP #line 240 "test_spec_scan.l" { @@ -2239,7 +2500,10 @@ YY_DECL } YY_BREAK + } + case 89: + { YY_RULE_SETUP #line 241 "test_spec_scan.l" { @@ -2247,7 +2511,10 @@ YY_DECL } YY_BREAK + } + case 90: + { YY_RULE_SETUP #line 242 "test_spec_scan.l" { @@ -2255,7 +2522,10 @@ YY_DECL } YY_BREAK + } + case 91: + { YY_RULE_SETUP #line 243 "test_spec_scan.l" { @@ -2263,7 +2533,10 @@ YY_DECL } YY_BREAK + } + case 92: + { YY_RULE_SETUP #line 245 "test_spec_scan.l" { @@ -2272,15 +2545,20 @@ YY_DECL } YY_BREAK + } + case 93: + { YY_RULE_SETUP #line 250 "test_spec_scan.l" { /* comment */ } YY_BREAK - case 94: + } + case 94: + { /* rule 94 can match eol */ YY_RULE_SETUP #line 251 "test_spec_scan.l" @@ -2289,14 +2567,20 @@ YY_DECL } YY_BREAK + } + case 95: + { YY_RULE_SETUP #line 252 "test_spec_scan.l" { /* whitespace */ } YY_BREAK + } + case 96: + { YY_RULE_SETUP #line 254 "test_spec_scan.l" { @@ -2305,7 +2589,10 @@ YY_DECL } YY_BREAK + } + case 97: + { YY_RULE_SETUP #line 255 "test_spec_scan.l" { @@ -2314,7 +2601,10 @@ YY_DECL } YY_BREAK + } + case 98: + { YY_RULE_SETUP #line 256 "test_spec_scan.l" { @@ -2323,7 +2613,10 @@ YY_DECL } YY_BREAK + } + case 99: + { YY_RULE_SETUP #line 258 "test_spec_scan.l" { @@ -2331,7 +2624,10 @@ YY_DECL } YY_BREAK + } + case 100: + { YY_RULE_SETUP #line 259 "test_spec_scan.l" { @@ -2339,7 +2635,10 @@ YY_DECL } YY_BREAK + } + case 101: + { YY_RULE_SETUP #line 260 "test_spec_scan.l" { @@ -2347,7 +2646,10 @@ YY_DECL } YY_BREAK + } + case 102: + { YY_RULE_SETUP #line 261 "test_spec_scan.l" { @@ -2355,7 +2657,10 @@ YY_DECL } YY_BREAK + } + case 103: + { YY_RULE_SETUP #line 262 "test_spec_scan.l" { @@ -2363,7 +2668,10 @@ YY_DECL } YY_BREAK + } + case 104: + { YY_RULE_SETUP #line 263 "test_spec_scan.l" { @@ -2371,7 +2679,10 @@ YY_DECL } YY_BREAK + } + case 105: + { YY_RULE_SETUP #line 264 "test_spec_scan.l" { @@ -2379,7 +2690,10 @@ YY_DECL } YY_BREAK + } + case 106: + { YY_RULE_SETUP #line 265 "test_spec_scan.l" { @@ -2387,7 +2701,10 @@ YY_DECL } YY_BREAK + } + case 107: + { YY_RULE_SETUP #line 266 "test_spec_scan.l" { @@ -2395,7 +2712,10 @@ YY_DECL } YY_BREAK + } + case 108: + { YY_RULE_SETUP #line 267 "test_spec_scan.l" { @@ -2403,7 +2723,10 @@ YY_DECL } YY_BREAK + } + case 109: + { YY_RULE_SETUP #line 268 "test_spec_scan.l" { @@ -2411,7 +2734,10 @@ YY_DECL } YY_BREAK + } + case 110: + { YY_RULE_SETUP #line 269 "test_spec_scan.l" { @@ -2419,7 +2745,10 @@ YY_DECL } YY_BREAK + } + case 111: + { YY_RULE_SETUP #line 270 "test_spec_scan.l" { @@ -2427,7 +2756,10 @@ YY_DECL } YY_BREAK + } + case 112: + { YY_RULE_SETUP #line 271 "test_spec_scan.l" { @@ -2435,7 +2767,10 @@ YY_DECL } YY_BREAK + } + case 113: + { YY_RULE_SETUP #line 272 "test_spec_scan.l" { @@ -2443,7 +2778,10 @@ YY_DECL } YY_BREAK + } + case 114: + { YY_RULE_SETUP #line 273 "test_spec_scan.l" { @@ -2451,7 +2789,10 @@ YY_DECL } YY_BREAK + } + case 115: + { YY_RULE_SETUP #line 274 "test_spec_scan.l" { @@ -2459,7 +2800,10 @@ YY_DECL } YY_BREAK + } + case 116: + { YY_RULE_SETUP #line 275 "test_spec_scan.l" { @@ -2467,7 +2811,10 @@ YY_DECL } YY_BREAK + } + case 117: + { YY_RULE_SETUP #line 276 "test_spec_scan.l" { @@ -2475,7 +2822,10 @@ YY_DECL } YY_BREAK + } + case 118: + { YY_RULE_SETUP #line 277 "test_spec_scan.l" { @@ -2483,7 +2833,10 @@ YY_DECL } YY_BREAK + } + case 119: + { YY_RULE_SETUP #line 278 "test_spec_scan.l" { @@ -2491,7 +2844,10 @@ YY_DECL } YY_BREAK + } + case 120: + { YY_RULE_SETUP #line 279 "test_spec_scan.l" { @@ -2499,7 +2855,10 @@ YY_DECL } YY_BREAK + } + case 121: + { YY_RULE_SETUP #line 280 "test_spec_scan.l" { @@ -2507,7 +2866,10 @@ YY_DECL } YY_BREAK + } + case 122: + { YY_RULE_SETUP #line 281 "test_spec_scan.l" { @@ -2515,7 +2877,10 @@ YY_DECL } YY_BREAK + } + case 123: + { YY_RULE_SETUP #line 282 "test_spec_scan.l" { @@ -2523,7 +2888,10 @@ YY_DECL } YY_BREAK + } + case 124: + { YY_RULE_SETUP #line 283 "test_spec_scan.l" { @@ -2531,7 +2899,10 @@ YY_DECL } YY_BREAK + } + case 125: + { YY_RULE_SETUP #line 284 "test_spec_scan.l" { @@ -2539,7 +2910,10 @@ YY_DECL } YY_BREAK + } + case 126: + { YY_RULE_SETUP #line 285 "test_spec_scan.l" { @@ -2547,7 +2921,10 @@ YY_DECL } YY_BREAK + } + case 127: + { YY_RULE_SETUP #line 286 "test_spec_scan.l" { @@ -2555,7 +2932,10 @@ YY_DECL } YY_BREAK + } + case 128: + { YY_RULE_SETUP #line 287 "test_spec_scan.l" { @@ -2563,7 +2943,10 @@ YY_DECL } YY_BREAK + } + case 129: + { YY_RULE_SETUP #line 288 "test_spec_scan.l" { @@ -2571,7 +2954,10 @@ YY_DECL } YY_BREAK + } + case 130: + { YY_RULE_SETUP #line 289 "test_spec_scan.l" { @@ -2579,7 +2965,10 @@ YY_DECL } YY_BREAK + } + case 131: + { YY_RULE_SETUP #line 290 "test_spec_scan.l" { @@ -2587,7 +2976,10 @@ YY_DECL } YY_BREAK + } + case 132: + { YY_RULE_SETUP #line 291 "test_spec_scan.l" { @@ -2596,7 +2988,10 @@ YY_DECL } YY_BREAK + } + case 133: + { YY_RULE_SETUP #line 292 "test_spec_scan.l" { @@ -2604,7 +2999,10 @@ YY_DECL } YY_BREAK + } + case 134: + { YY_RULE_SETUP #line 293 "test_spec_scan.l" { @@ -2612,7 +3010,10 @@ YY_DECL } YY_BREAK + } + case 135: + { YY_RULE_SETUP #line 294 "test_spec_scan.l" { @@ -2620,7 +3021,10 @@ YY_DECL } YY_BREAK + } + case 136: + { YY_RULE_SETUP #line 295 "test_spec_scan.l" { @@ -2628,7 +3032,10 @@ YY_DECL } YY_BREAK + } + case 137: + { YY_RULE_SETUP #line 297 "test_spec_scan.l" { @@ -2637,8 +3044,10 @@ YY_DECL } YY_BREAK - case 138: + } + case 138: + { /* rule 138 can match eol */ YY_RULE_SETUP #line 302 "test_spec_scan.l" @@ -2649,7 +3058,10 @@ YY_DECL } YY_BREAK + } + case 139: + { YY_RULE_SETUP #line 308 "test_spec_scan.l" { @@ -2664,7 +3076,10 @@ YY_DECL } YY_BREAK + } + case 140: + { YY_RULE_SETUP #line 318 "test_spec_scan.l" { @@ -2679,7 +3094,10 @@ YY_DECL } YY_BREAK + } + case 141: + { YY_RULE_SETUP #line 328 "test_spec_scan.l" { @@ -2688,14 +3106,20 @@ YY_DECL } YY_BREAK + } + case 142: + { YY_RULE_SETUP #line 333 "test_spec_scan.l" { /* skip whitespace before service name */ } YY_BREAK + } + case 143: + { YY_RULE_SETUP #line 335 "test_spec_scan.l" { @@ -2705,8 +3129,10 @@ YY_DECL } YY_BREAK - case 144: + } + case 144: + { /* rule 144 can match eol */ YY_RULE_SETUP #line 341 "test_spec_scan.l" @@ -2716,7 +3142,10 @@ YY_DECL } YY_BREAK + } + case 145: + { YY_RULE_SETUP #line 346 "test_spec_scan.l" { @@ -2731,8 +3160,10 @@ YY_DECL } YY_BREAK - case 146: + } + case 146: + { /* rule 146 can match eol */ YY_RULE_SETUP #line 354 "test_spec_scan.l" @@ -2742,19 +3173,26 @@ YY_DECL } YY_BREAK + } + case 147: + { YY_RULE_SETUP #line 359 "test_spec_scan.l" ECHO; YY_BREAK + } + #line 2301 "test_spec_scan.c" case YY_STATE_EOF(INITIAL): case YY_STATE_EOF(CLUSTER_BODY): case YY_STATE_EOF(STEP_BODY): case YY_STATE_EOF(EXEC_ARGS): case YY_STATE_EOF(EXEC_ARGS_REST): + { yyterminate(); + } case YY_END_OF_BUFFER: { @@ -2788,8 +3226,8 @@ YY_DECL * end-of-buffer state). Contrast this with the test * in input(). */ - if ((yy_c_buf_p) <= - &YY_CURRENT_BUFFER_LVALUE->yy_ch_buf[(yy_n_chars)]) + if ((yy_c_buf_p) <= &YY_CURRENT_BUFFER_LVALUE->yy_ch_buf[(yy_n_chars)] + ) { /* This was really a NUL. */ yy_state_type yy_next_state; @@ -2859,6 +3297,7 @@ YY_DECL } case EOB_ACT_CONTINUE_SCAN: + { (yy_c_buf_p) = (yytext_ptr) + yy_amount_of_matched_text; @@ -2867,8 +3306,10 @@ YY_DECL yy_cp = (yy_c_buf_p); yy_bp = (yytext_ptr) + YY_MORE_ADJ; goto yy_match; + } case EOB_ACT_LAST_MATCH: + { (yy_c_buf_p) = &YY_CURRENT_BUFFER_LVALUE->yy_ch_buf[(yy_n_chars)]; @@ -2877,14 +3318,17 @@ YY_DECL yy_cp = (yy_c_buf_p); yy_bp = (yytext_ptr) + YY_MORE_ADJ; goto yy_find_action; + } } } break; } default: + { YY_FATAL_ERROR( "fatal flex scanner internal error--no action found"); + } } /* end of action switch */ } /* end of scanning one token */ } /* end of user's declarations */ @@ -3153,7 +3597,7 @@ input(void) switch (yy_get_next_buffer()) { case EOB_ACT_LAST_MATCH: - + { /* This happens because yy_g_n_b() * sees that we've accumulated a * token and flags that we need to @@ -3166,6 +3610,7 @@ input(void) /* Reset buffer status. */ yyrestart(yyin); + } /*FALLTHROUGH*/ @@ -3188,8 +3633,10 @@ input(void) } case EOB_ACT_CONTINUE_SCAN: + { (yy_c_buf_p) = (yytext_ptr) + offset; break; + } } } } @@ -3633,18 +4080,18 @@ yy_fatal_error(const char *msg) #undef yyless #define yyless(n) \ - do \ - { \ - /* Undo effects of setting up yytext. */ \ - yy_size_t yyless_macro_arg = (n); \ - YY_LESS_LINENO(yyless_macro_arg); \ - yytext[yyleng] = (yy_hold_char); \ - (yy_c_buf_p) = yytext + yyless_macro_arg; \ - (yy_hold_char) = *(yy_c_buf_p); \ - *(yy_c_buf_p) = '\0'; \ - yyleng = yyless_macro_arg; \ - } \ - while (0) + do \ + { \ + /* Undo effects of setting up yytext. */ \ + yy_size_t yyless_macro_arg = (n); \ + YY_LESS_LINENO(yyless_macro_arg); \ + yytext[yyleng] = (yy_hold_char); \ + (yy_c_buf_p) = yytext + yyless_macro_arg; \ + (yy_hold_char) = *(yy_c_buf_p); \ + *(yy_c_buf_p) = '\0'; \ + yyleng = yyless_macro_arg; \ + } \ + while (0) /* Accessor methods (get/set functions) to struct members. */ diff --git a/src/monitor/formation_metadata.c b/src/monitor/formation_metadata.c index debdef1e7..d4974fb6b 100644 --- a/src/monitor/formation_metadata.c +++ b/src/monitor/formation_metadata.c @@ -474,8 +474,10 @@ FormationKindToString(FormationKind kind) } default: + { ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("unknown formation kind value %d", kind))); + } } /* keep compiler happy */ diff --git a/src/monitor/health_check_worker.c b/src/monitor/health_check_worker.c index b19f7fa6a..36dc0fe5d 100644 --- a/src/monitor/health_check_worker.c +++ b/src/monitor/health_check_worker.c @@ -52,9 +52,9 @@ * that TLS is not necessarily used, because no secret information is sent. */ #define CONN_INFO_TEMPLATE \ - "host=%s port=%u user=pgautofailover_monitor " \ - "password=pgautofailover_monitor dbname=postgres " \ - "connect_timeout=%u" + "host=%s port=%u user=pgautofailover_monitor " \ + "password=pgautofailover_monitor dbname=postgres " \ + "connect_timeout=%u" #define MAX_CONN_INFO_SIZE 1024 diff --git a/src/monitor/node_metadata.c b/src/monitor/node_metadata.c index f80d2572b..34fb6adc6 100644 --- a/src/monitor/node_metadata.c +++ b/src/monitor/node_metadata.c @@ -1642,9 +1642,11 @@ SyncStateToString(SyncState pgsrSyncState) } default: + { ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("unknown SyncState enum value %d", pgsrSyncState))); + } } /* keep compiler happy */ diff --git a/src/monitor/node_metadata.h b/src/monitor/node_metadata.h index 607cf8bd9..62280b411 100644 --- a/src/monitor/node_metadata.h +++ b/src/monitor/node_metadata.h @@ -50,31 +50,31 @@ #define Anum_pgautofailover_node_nodecluster 21 #define AUTO_FAILOVER_NODE_TABLE_ALL_COLUMNS \ - "formationid, " \ - "nodeid, " \ - "groupid, " \ - "nodename, " \ - "nodehost, " \ - "nodeport, " \ - "sysidentifier, " \ - "goalstate, " \ - "reportedstate, " \ - "reportedpgisrunning, " \ - "reportedrepstate, " \ - "reporttime, " \ - "reportedtli, " \ - "reportedlsn, " \ - "walreporttime, " \ - "health, " \ - "healthchecktime, " \ - "statechangetime, " \ - "candidatepriority, " \ - "replicationquorum, " \ - "nodecluster" + "formationid, " \ + "nodeid, " \ + "groupid, " \ + "nodename, " \ + "nodehost, " \ + "nodeport, " \ + "sysidentifier, " \ + "goalstate, " \ + "reportedstate, " \ + "reportedpgisrunning, " \ + "reportedrepstate, " \ + "reporttime, " \ + "reportedtli, " \ + "reportedlsn, " \ + "walreporttime, " \ + "health, " \ + "healthchecktime, " \ + "statechangetime, " \ + "candidatepriority, " \ + "replicationquorum, " \ + "nodecluster" #define SELECT_ALL_FROM_AUTO_FAILOVER_NODE_TABLE \ - "SELECT " AUTO_FAILOVER_NODE_TABLE_ALL_COLUMNS " FROM " AUTO_FAILOVER_NODE_TABLE + "SELECT " AUTO_FAILOVER_NODE_TABLE_ALL_COLUMNS " FROM " AUTO_FAILOVER_NODE_TABLE /* pg_stat_replication.sync_state: "sync", "async", "quorum", "potential" */ typedef enum SyncState @@ -104,7 +104,7 @@ typedef enum SyncState */ #define NODE_FORMAT "node %lld \"%s\" (%s:%d)" #define NODE_FORMAT_ARGS(node) \ - (long long) node->nodeId, node->nodeName, node->nodeHost, node->nodePort + (long long) node->nodeId, node->nodeName, node->nodeHost, node->nodePort /* * AutoFailoverNode represents a Postgres node that is being tracked by the diff --git a/src/monitor/version_compat.h b/src/monitor/version_compat.h index 82f36abc0..f11dfb019 100644 --- a/src/monitor/version_compat.h +++ b/src/monitor/version_compat.h @@ -27,10 +27,10 @@ #define DEFAULT_XLOG_SEG_SIZE XLOG_SEG_SIZE #define BackgroundWorkerInitializeConnection(dbname, username, flags) \ - BackgroundWorkerInitializeConnection(dbname, username) + BackgroundWorkerInitializeConnection(dbname, username) #define BackgroundWorkerInitializeConnectionByOid(dboid, useroid, flags) \ - BackgroundWorkerInitializeConnectionByOid(dboid, useroid) + BackgroundWorkerInitializeConnectionByOid(dboid, useroid) #include "nodes/pg_list.h" From a0ab8e600e37e37556ccaa2576073330fc761749 Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Tue, 7 Jul 2026 15:25:06 +0200 Subject: [PATCH 32/68] style: re-apply citus_indent from repo root (matching CI) --- src/bin/common/ini_file.h | 30 +- src/bin/common/pgctl.c | 2 +- src/bin/common/pgsetup.c | 2 +- src/bin/common/pgsetup.h | 4 +- src/bin/common/pgsql.c | 2 +- src/bin/common/pgsql.h | 38 +- src/bin/pg_autoctl/cli_common.h | 70 +- src/bin/pg_autoctl/config.h | 2 +- src/bin/pg_autoctl/demoapp.c | 4 +- src/bin/pg_autoctl/fsm.c | 116 +- src/bin/pg_autoctl/keeper.c | 4 +- src/bin/pg_autoctl/keeper_config.c | 298 ++-- src/bin/pg_autoctl/keeper_config.h | 2 +- src/bin/pg_autoctl/monitor_config.c | 118 +- src/bin/pg_autoctl/nodespec.c | 10 +- src/bin/pg_autoctl/primary_standby.c | 58 +- src/bin/pg_autoctl/systemd_config.c | 66 +- src/bin/pgaftest/compose_gen.c | 80 +- src/bin/pgaftest/test_runner.c | 4 +- src/bin/pgaftest/test_spec_parse.c | 2208 +++++++++++++------------- src/bin/pgaftest/test_spec_scan.c | 150 +- src/monitor/health_check_worker.c | 6 +- src/monitor/node_metadata.h | 46 +- src/monitor/version_compat.h | 4 +- 24 files changed, 1662 insertions(+), 1662 deletions(-) diff --git a/src/bin/common/ini_file.h b/src/bin/common/ini_file.h index a19587a7c..7e3d61918 100644 --- a/src/bin/common/ini_file.h +++ b/src/bin/common/ini_file.h @@ -59,38 +59,38 @@ typedef struct IniOption } IniOption; #define make_int_option(section, name, optName, required, value) \ - { INI_INT_T, section, name, optName, required, false, \ - NULL, -1, -1, NULL, NULL, value } + { INI_INT_T, section, name, optName, required, false, \ + NULL, -1, -1, NULL, NULL, value } #define make_int_option_default(section, name, optName, \ required, value, default) \ - { INI_INT_T, section, name, optName, required, false, \ - NULL, default, -1, NULL, NULL, value } + { INI_INT_T, section, name, optName, required, false, \ + NULL, default, -1, NULL, NULL, value } #define make_string_option(section, name, optName, required, value) \ - { INI_STRING_T, section, name, optName, required, false, \ - NULL, -1, -1, value, NULL, NULL } + { INI_STRING_T, section, name, optName, required, false, \ + NULL, -1, -1, value, NULL, NULL } #define make_string_option_default(section, name, optName, required, \ value, default) \ - { INI_STRING_T, section, name, optName, required, false, \ - default, -1, -1, value, NULL, NULL } + { INI_STRING_T, section, name, optName, required, false, \ + default, -1, -1, value, NULL, NULL } #define make_strbuf_option(section, name, optName, required, size, value) \ - { INI_STRBUF_T, section, name, optName, required, false, \ - NULL, -1, size, NULL, value, NULL } + { INI_STRBUF_T, section, name, optName, required, false, \ + NULL, -1, size, NULL, value, NULL } #define make_strbuf_compat_option(section, name, size, value) \ - { INI_STRBUF_T, section, name, NULL, false, true, \ - NULL, -1, size, NULL, value, NULL } + { INI_STRBUF_T, section, name, NULL, false, true, \ + NULL, -1, size, NULL, value, NULL } #define make_strbuf_option_default(section, name, optName, required, \ size, value, default) \ - { INI_STRBUF_T, section, name, optName, required, false, \ - default, -1, size, NULL, value, NULL } + { INI_STRBUF_T, section, name, optName, required, false, \ + default, -1, size, NULL, value, NULL } #define INI_OPTION_LAST \ - { INI_END_T, NULL, NULL, NULL, false, false, NULL, -1, -1, NULL, NULL, NULL } + { INI_END_T, NULL, NULL, NULL, false, false, NULL, -1, -1, NULL, NULL, NULL } bool read_ini_file(const char *filename, IniOption *opts); bool parse_ini_buffer(const char *filename, diff --git a/src/bin/common/pgctl.c b/src/bin/common/pgctl.c index 893a1b799..8ad756ae3 100644 --- a/src/bin/common/pgctl.c +++ b/src/bin/common/pgctl.c @@ -35,7 +35,7 @@ #include "runprogram.h" #define AUTOCTL_CONF_INCLUDE_COMMENT \ - " # Auto-generated by pg_auto_failover, do not remove\n" + " # Auto-generated by pg_auto_failover, do not remove\n" #define AUTOCTL_CONF_INCLUDE_LINE "include '" AUTOCTL_DEFAULTS_CONF_FILENAME "'" #define AUTOCTL_SB_CONF_INCLUDE_LINE "include '" AUTOCTL_STANDBY_CONF_FILENAME "'" diff --git a/src/bin/common/pgsetup.c b/src/bin/common/pgsetup.c index f1bed87f0..c9e0b0ecf 100644 --- a/src/bin/common/pgsetup.c +++ b/src/bin/common/pgsetup.c @@ -386,7 +386,7 @@ pg_setup_init(PostgresSetup *pgSetup, else { log_debug("Found PostgreSQL system %" PRIu64 " at \"%s\", " - "version %u, catalog version %u", + "version %u, catalog version %u", pgSetup->control.system_identifier, pgSetup->pgdata, pgSetup->control.pg_control_version, diff --git a/src/bin/common/pgsetup.h b/src/bin/common/pgsetup.h index 4fbfb383f..f4d4b182d 100644 --- a/src/bin/common/pgsetup.h +++ b/src/bin/common/pgsetup.h @@ -135,11 +135,11 @@ typedef enum PgInstanceKind #define NODE_KIND_CITUS_ANY \ - (NODE_KIND_CITUS_COORDINATOR | NODE_KIND_CITUS_WORKER) + (NODE_KIND_CITUS_COORDINATOR | NODE_KIND_CITUS_WORKER) #define IS_CITUS_INSTANCE_KIND(x) \ - (x == NODE_KIND_CITUS_COORDINATOR \ + (x == NODE_KIND_CITUS_COORDINATOR \ || x == NODE_KIND_CITUS_WORKER) #define pgKind_matches(x, y) ((x & y) != 0) diff --git a/src/bin/common/pgsql.c b/src/bin/common/pgsql.c index 1f2c1ff01..4fe32a084 100644 --- a/src/bin/common/pgsql.c +++ b/src/bin/common/pgsql.c @@ -602,7 +602,7 @@ pgsql_open_connection(PGSQL *pgsql) * to help anyone. A good trade-off seems to be a warning every 30s. */ #define SHOULD_WARN_AGAIN(duration) \ - (INSTR_TIME_GET_MILLISEC(duration) > 30000) + (INSTR_TIME_GET_MILLISEC(duration) > 30000) /* * pgsql_retry_open_connection loops over a PQping call until the remote server diff --git a/src/bin/common/pgsql.h b/src/bin/common/pgsql.h index b5e3f02c7..978f6b172 100644 --- a/src/bin/common/pgsql.h +++ b/src/bin/common/pgsql.h @@ -293,29 +293,29 @@ typedef struct SingleValueResultContext #define CHECK__SETTINGS_SQL \ - "select bool_and(ok) " \ - "from (" \ - "select current_setting('max_wal_senders')::int >= 12" \ - " union all " \ - "select current_setting('max_replication_slots')::int >= 12" \ - " union all " \ - "select current_setting('wal_level') in ('replica', 'logical')" \ - " union all " \ - "select current_setting('wal_log_hints') = 'on'" + "select bool_and(ok) " \ + "from (" \ + "select current_setting('max_wal_senders')::int >= 12" \ + " union all " \ + "select current_setting('max_replication_slots')::int >= 12" \ + " union all " \ + "select current_setting('wal_level') in ('replica', 'logical')" \ + " union all " \ + "select current_setting('wal_log_hints') = 'on'" #define CHECK_POSTGRESQL_NODE_SETTINGS_SQL \ - CHECK__SETTINGS_SQL \ - ") as t(ok) " + CHECK__SETTINGS_SQL \ + ") as t(ok) " #define CHECK_CITUS_NODE_SETTINGS_SQL \ - CHECK__SETTINGS_SQL \ - " union all " \ - "select lib = 'citus' " \ - "from unnest(string_to_array(" \ - "current_setting('shared_preload_libraries'), ',') " \ - " || array['not citus']) " \ - "with ordinality ast(lib, n) where n = 1" \ - ") as t(ok) " + CHECK__SETTINGS_SQL \ + " union all " \ + "select lib = 'citus' " \ + "from unnest(string_to_array(" \ + "current_setting('shared_preload_libraries'), ',') " \ + " || array['not citus']) " \ + "with ordinality ast(lib, n) where n = 1" \ + ") as t(ok) " bool pgsql_init(PGSQL *pgsql, char *url, ConnectionType connectionType); diff --git a/src/bin/pg_autoctl/cli_common.h b/src/bin/pg_autoctl/cli_common.h index 68d5ddbad..66c62fa40 100644 --- a/src/bin/pg_autoctl/cli_common.h +++ b/src/bin/pg_autoctl/cli_common.h @@ -36,47 +36,47 @@ extern int ssl_flag; extern int monitorDisabledNodeId; #define KEEPER_CLI_SSL_OPTIONS \ - " --ssl-self-signed setup network encryption using self signed certificates (does NOT protect against MITM)\n" \ - " --ssl-mode use that sslmode in connection strings\n" \ - " --ssl-ca-file set the Postgres ssl_ca_file to that file path\n" \ - " --ssl-crl-file set the Postgres ssl_crl_file to that file path\n" \ - " --no-ssl don't enable network encryption (NOT recommended, prefer --ssl-self-signed)\n" \ - " --server-key set the Postgres ssl_key_file to that file path\n" \ - " --server-cert set the Postgres ssl_cert_file to that file path\n" + " --ssl-self-signed setup network encryption using self signed certificates (does NOT protect against MITM)\n" \ + " --ssl-mode use that sslmode in connection strings\n" \ + " --ssl-ca-file set the Postgres ssl_ca_file to that file path\n" \ + " --ssl-crl-file set the Postgres ssl_crl_file to that file path\n" \ + " --no-ssl don't enable network encryption (NOT recommended, prefer --ssl-self-signed)\n" \ + " --server-key set the Postgres ssl_key_file to that file path\n" \ + " --server-cert set the Postgres ssl_cert_file to that file path\n" #define KEEPER_CLI_WORKER_SETUP_OPTIONS \ - " --pgctl path to pg_ctl\n" \ - " --pgdata path to data directory\n" \ - " --pghost PostgreSQL's hostname\n" \ - " --pgport PostgreSQL's port number\n" \ - " --listen PostgreSQL's listen_addresses\n" \ - " --username PostgreSQL's username\n" \ - " --dbname PostgreSQL's database name\n" \ - " --proxyport Proxy's port number\n" \ - " --name pg_auto_failover node name\n" \ - " --hostname hostname used to connect from the other nodes\n" \ - " --formation pg_auto_failover formation\n" \ - " --group pg_auto_failover group Id\n" \ - " --monitor pg_auto_failover Monitor Postgres URL\n" \ - KEEPER_CLI_SSL_OPTIONS + " --pgctl path to pg_ctl\n" \ + " --pgdata path to data directory\n" \ + " --pghost PostgreSQL's hostname\n" \ + " --pgport PostgreSQL's port number\n" \ + " --listen PostgreSQL's listen_addresses\n" \ + " --username PostgreSQL's username\n" \ + " --dbname PostgreSQL's database name\n" \ + " --proxyport Proxy's port number\n" \ + " --name pg_auto_failover node name\n" \ + " --hostname hostname used to connect from the other nodes\n" \ + " --formation pg_auto_failover formation\n" \ + " --group pg_auto_failover group Id\n" \ + " --monitor pg_auto_failover Monitor Postgres URL\n" \ + KEEPER_CLI_SSL_OPTIONS #define KEEPER_CLI_NON_WORKER_SETUP_OPTIONS \ - " --pgctl path to pg_ctl\n" \ - " --pgdata path to data directory\n" \ - " --pghost PostgreSQL's hostname\n" \ - " --pgport PostgreSQL's port number\n" \ - " --listen PostgreSQL's listen_addresses\n" \ - " --username PostgreSQL's username\n" \ - " --dbname PostgreSQL's database name\n" \ - " --name pg_auto_failover node name\n" \ - " --hostname hostname used to connect from the other nodes\n" \ - " --formation pg_auto_failover formation\n" \ - " --group pg_auto_failover group Id\n" \ - " --monitor pg_auto_failover Monitor Postgres URL\n" \ - KEEPER_CLI_SSL_OPTIONS + " --pgctl path to pg_ctl\n" \ + " --pgdata path to data directory\n" \ + " --pghost PostgreSQL's hostname\n" \ + " --pgport PostgreSQL's port number\n" \ + " --listen PostgreSQL's listen_addresses\n" \ + " --username PostgreSQL's username\n" \ + " --dbname PostgreSQL's database name\n" \ + " --name pg_auto_failover node name\n" \ + " --hostname hostname used to connect from the other nodes\n" \ + " --formation pg_auto_failover formation\n" \ + " --group pg_auto_failover group Id\n" \ + " --monitor pg_auto_failover Monitor Postgres URL\n" \ + KEEPER_CLI_SSL_OPTIONS #define CLI_PGDATA_OPTION \ - " --pgdata path to data directory\n" \ + " --pgdata path to data directory\n" \ #define CLI_PGDATA_USAGE " [ --pgdata ] [ --json ] " diff --git a/src/bin/pg_autoctl/config.h b/src/bin/pg_autoctl/config.h index 4d036c90f..325474b2b 100644 --- a/src/bin/pg_autoctl/config.h +++ b/src/bin/pg_autoctl/config.h @@ -77,7 +77,7 @@ pgAutoCtlNodeRole ProbeConfigurationFileRole(const char *filename); #define strneq(x, y) \ - ((x != NULL) && (y != NULL) && (strcmp(x, y) != 0)) + ((x != NULL) && (y != NULL) && (strcmp(x, y) != 0)) bool config_accept_new_ssloptions(PostgresSetup *pgSetup, PostgresSetup *newPgSetup); diff --git a/src/bin/pg_autoctl/demoapp.c b/src/bin/pg_autoctl/demoapp.c index c12bd3b11..609c75c97 100644 --- a/src/bin/pg_autoctl/demoapp.c +++ b/src/bin/pg_autoctl/demoapp.c @@ -546,10 +546,10 @@ demoapp_update_client_failovers(const char *pguri, int clientId, int failovers) */ #if PG_MAJORVERSION_NUM < 15 #define random_between(M, N) \ - ((M) + pg_lrand48() / (RAND_MAX / ((N) -(M) +1) + 1)) + ((M) + pg_lrand48() / (RAND_MAX / ((N) -(M) +1) + 1)) #else #define random_between(M, N) \ - ((M) + pg_prng_uint32(&prng_state) / (RAND_MAX / ((N) -(M) +1) + 1)) + ((M) + pg_prng_uint32(&prng_state) / (RAND_MAX / ((N) -(M) +1) + 1)) #endif /* diff --git a/src/bin/pg_autoctl/fsm.c b/src/bin/pg_autoctl/fsm.c index 38ad2714d..9680b6d81 100644 --- a/src/bin/pg_autoctl/fsm.c +++ b/src/bin/pg_autoctl/fsm.c @@ -30,156 +30,156 @@ * Comments displayed in the logs when state changes. */ #define COMMENT_INIT_TO_SINGLE \ - "Start as a single node" + "Start as a single node" #define COMMENT_PRIMARY_TO_SINGLE \ - "Other node was forcibly removed, now single" + "Other node was forcibly removed, now single" #define COMMENT_DEMOTED_TO_SINGLE \ - "Was demoted after a failure, " \ - "but secondary was forcibly removed" + "Was demoted after a failure, " \ + "but secondary was forcibly removed" #define COMMENT_LOST_PRIMARY \ - "Primary was forcibly removed" + "Primary was forcibly removed" #define COMMENT_REPLICATION_TO_SINGLE \ - "Went down to force the primary to time out, " \ - "but then it was removed" + "Went down to force the primary to time out, " \ + "but then it was removed" #define COMMENT_SINGLE_TO_WAIT_PRIMARY \ - "A new secondary was added" + "A new secondary was added" #define COMMENT_PRIMARY_TO_WAIT_PRIMARY \ - "Secondary became unhealthy" + "Secondary became unhealthy" #define COMMENT_PRIMARY_TO_JOIN_PRIMARY \ - "A new secondary was added" + "A new secondary was added" #define COMMENT_PRIMARY_TO_DRAINING \ - "A failover occurred, stopping writes " + "A failover occurred, stopping writes " #define COMMENT_PRIMARY_TO_PREPARE_MAINTENANCE \ - "Promoting the standby to enable maintenance on the " \ - "primary, stopping Postgres " + "Promoting the standby to enable maintenance on the " \ + "primary, stopping Postgres " #define COMMENT_PRIMARY_TO_MAINTENANCE \ - "Setting up Postgres in standby mode for maintenance operations" + "Setting up Postgres in standby mode for maintenance operations" #define COMMENT_PRIMARY_TO_MAINTENANCE_PROMOTE_SECONDARY \ - "Promoting the standby to enable maintenance on the primary" + "Promoting the standby to enable maintenance on the primary" #define COMMENT_PRIMARY_TO_DEMOTED \ - "A failover occurred, no longer primary" + "A failover occurred, no longer primary" #define COMMENT_DRAINING_TO_DEMOTED \ - "Demoted after a failover, no longer primary" + "Demoted after a failover, no longer primary" #define COMMENT_DRAINING_TO_DEMOTE_TIMEOUT \ - "Secondary confirms it’s receiving no more writes" + "Secondary confirms it’s receiving no more writes" #define COMMENT_DEMOTE_TIMEOUT_TO_DEMOTED \ - "Demote timeout expired" + "Demote timeout expired" #define COMMENT_STOP_REPLICATION_TO_WAIT_PRIMARY \ - "Confirmed promotion with the monitor" + "Confirmed promotion with the monitor" #define COMMENT_WAIT_PRIMARY_TO_PRIMARY \ - "A healthy secondary appeared" + "A healthy secondary appeared" #define COMMENT_JOIN_PRIMARY_TO_PRIMARY \ - "A healthy secondary appeared" + "A healthy secondary appeared" #define COMMENT_DEMOTE_TO_PRIMARY \ - "Detected a network partition, " \ - "but monitor didn't do failover" + "Detected a network partition, " \ + "but monitor didn't do failover" #define COMMENT_WAIT_STANDBY_TO_CATCHINGUP \ - "The primary is now ready to accept a standby" + "The primary is now ready to accept a standby" #define COMMENT_DEMOTED_TO_CATCHINGUP \ - "A new primary is available. " \ - "First, try to rewind. If that fails, do a pg_basebackup." + "A new primary is available. " \ + "First, try to rewind. If that fails, do a pg_basebackup." #define COMMENT_SECONDARY_TO_CATCHINGUP \ - "Failed to report back to the monitor, " \ - "not eligible for promotion" + "Failed to report back to the monitor, " \ + "not eligible for promotion" #define COMMENT_CATCHINGUP_TO_SECONDARY \ - "Convinced the monitor that I'm up and running, " \ - "and eligible for promotion again" + "Convinced the monitor that I'm up and running, " \ + "and eligible for promotion again" #define COMMENT_SECONDARY_TO_PREP_PROMOTION \ - "Stop traffic to primary, " \ - "wait for it to finish draining." + "Stop traffic to primary, " \ + "wait for it to finish draining." #define COMMENT_PROMOTION_TO_STOP_REPLICATION \ - "Prevent against split-brain situations." + "Prevent against split-brain situations." #define COMMENT_INIT_TO_WAIT_STANDBY \ - "Start following a primary" + "Start following a primary" #define COMMENT_SECONARY_TO_WAIT_STANDBY \ - "Registering to a new monitor" + "Registering to a new monitor" #define COMMENT_SECONDARY_TO_WAIT_MAINTENANCE \ - "Waiting for the primary to disable sync replication before " \ - "going to maintenance." + "Waiting for the primary to disable sync replication before " \ + "going to maintenance." #define COMMENT_SECONDARY_TO_MAINTENANCE \ - "Suspending standby for manual maintenance." + "Suspending standby for manual maintenance." #define COMMENT_MAINTENANCE_TO_CATCHINGUP \ - "Restarting standby after manual maintenance is done." + "Restarting standby after manual maintenance is done." #define COMMENT_BLOCKED_WRITES \ - "Promoting a Citus Worker standby after having blocked writes " \ - "from the coordinator." + "Promoting a Citus Worker standby after having blocked writes " \ + "from the coordinator." #define COMMENT_PRIMARY_TO_APPLY_SETTINGS \ - "Apply new pg_auto_failover settings (synchronous_standby_names)" + "Apply new pg_auto_failover settings (synchronous_standby_names)" #define COMMENT_APPLY_SETTINGS_TO_PRIMARY \ - "Back to primary state after having applied new pg_auto_failover settings" + "Back to primary state after having applied new pg_auto_failover settings" #define COMMENT_SECONDARY_TO_REPORT_LSN \ - "Reporting the last write-ahead log location received" + "Reporting the last write-ahead log location received" #define COMMENT_DRAINING_TO_REPORT_LSN \ - "Reporting the last write-ahead log location after draining" + "Reporting the last write-ahead log location after draining" #define COMMENT_DEMOTED_TO_REPORT_LSN \ - "Reporting the last write-ahead log location after being demoted" + "Reporting the last write-ahead log location after being demoted" #define COMMENT_REPORT_LSN_TO_PREP_PROMOTION \ - "Stop traffic to primary, " \ - "wait for it to finish draining." + "Stop traffic to primary, " \ + "wait for it to finish draining." #define COMMENT_REPORT_LSN_TO_FAST_FORWARD \ - "Fetching missing WAL bits from another standby before promotion" + "Fetching missing WAL bits from another standby before promotion" #define COMMENT_REPORT_LSN_TO_SINGLE \ - "There is no other node anymore, promote this node" + "There is no other node anymore, promote this node" #define COMMENT_FOLLOW_NEW_PRIMARY \ - "Switch replication to the new primary" + "Switch replication to the new primary" #define COMMENT_REPORT_LSN_TO_JOIN_SECONDARY \ - "A failover candidate has been selected, stop replication" + "A failover candidate has been selected, stop replication" #define COMMENT_JOIN_SECONDARY_TO_SECONDARY \ - "Failover is done, we have a new primary to follow" + "Failover is done, we have a new primary to follow" #define COMMENT_FAST_FORWARD_TO_PREP_PROMOTION \ - "Got the missing WAL bytes, promoted" + "Got the missing WAL bytes, promoted" #define COMMENT_INIT_TO_REPORT_LSN \ - "Creating a new node from a standby node that is not a candidate." + "Creating a new node from a standby node that is not a candidate." #define COMMENT_DROPPED_TO_REPORT_LSN \ - "This node is being reinitialized after having been dropped" + "This node is being reinitialized after having been dropped" #define COMMENT_ANY_TO_DROPPED \ - "This node is being dropped from the monitor" + "This node is being dropped from the monitor" /* diff --git a/src/bin/pg_autoctl/keeper.c b/src/bin/pg_autoctl/keeper.c index 5b8083ac1..782e0d1e5 100644 --- a/src/bin/pg_autoctl/keeper.c +++ b/src/bin/pg_autoctl/keeper.c @@ -388,7 +388,7 @@ ReportPgIsRunning(Keeper *keeper) */ log_error("Failed to restart PostgreSQL %d times in the " "last %" PRIu64 "s, reporting PostgreSQL not running to " - "the pg_auto_failover monitor.", + "the pg_auto_failover monitor.", postgres->pgStartRetries, now - postgres->pgFirstStartFailureTs); @@ -638,7 +638,7 @@ keeper_state_check_postgres(Keeper *keeper, PostgresControlData *control) * are doing anymore. */ log_error("Unknown PostgreSQL system identifier: %" PRIu64 ", " - "expected %" PRIu64, + "expected %" PRIu64, keeperState->system_identifier, control->system_identifier); return false; diff --git a/src/bin/pg_autoctl/keeper_config.c b/src/bin/pg_autoctl/keeper_config.c index 2cdcf74bf..1459336df 100644 --- a/src/bin/pg_autoctl/keeper_config.c +++ b/src/bin/pg_autoctl/keeper_config.c @@ -22,25 +22,25 @@ #include "pgctl.h" #define OPTION_AUTOCTL_ROLE(config) \ - make_strbuf_option_default("pg_autoctl", "role", NULL, true, NAMEDATALEN, \ - config->role, KEEPER_ROLE) + make_strbuf_option_default("pg_autoctl", "role", NULL, true, NAMEDATALEN, \ + config->role, KEEPER_ROLE) #define OPTION_AUTOCTL_MONITOR(config) \ - make_strbuf_option("pg_autoctl", "monitor", "monitor", false, MAXCONNINFO, \ - config->monitor_pguri) + make_strbuf_option("pg_autoctl", "monitor", "monitor", false, MAXCONNINFO, \ + config->monitor_pguri) #define OPTION_AUTOCTL_FORMATION(config) \ - make_strbuf_option_default("pg_autoctl", "formation", "formation", \ - true, NAMEDATALEN, \ - config->formation, FORMATION_DEFAULT) + make_strbuf_option_default("pg_autoctl", "formation", "formation", \ + true, NAMEDATALEN, \ + config->formation, FORMATION_DEFAULT) #define OPTION_AUTOCTL_GROUPID(config) \ - make_int_option("pg_autoctl", "group", "group", false, &(config->groupId)) + make_int_option("pg_autoctl", "group", "group", false, &(config->groupId)) #define OPTION_AUTOCTL_NAME(config) \ - make_strbuf_option_default("pg_autoctl", "name", "name", \ - false, _POSIX_HOST_NAME_MAX, \ - config->name, "") + make_strbuf_option_default("pg_autoctl", "name", "name", \ + false, _POSIX_HOST_NAME_MAX, \ + config->name, "") /* * --hostname used to be --nodename, and we need to support transition from the @@ -50,212 +50,212 @@ * As a result HOSTNAME is marked not required and NODENAME is marked compat. */ #define OPTION_AUTOCTL_HOSTNAME(config) \ - make_strbuf_option("pg_autoctl", "hostname", "hostname", \ - false, _POSIX_HOST_NAME_MAX, config->hostname) + make_strbuf_option("pg_autoctl", "hostname", "hostname", \ + false, _POSIX_HOST_NAME_MAX, config->hostname) #define OPTION_AUTOCTL_NODENAME(config) \ - make_strbuf_compat_option("pg_autoctl", "nodename", \ - _POSIX_HOST_NAME_MAX, config->hostname) + make_strbuf_compat_option("pg_autoctl", "nodename", \ + _POSIX_HOST_NAME_MAX, config->hostname) #define OPTION_AUTOCTL_NODEKIND(config) \ - make_strbuf_option("pg_autoctl", "nodekind", NULL, false, NAMEDATALEN, \ - config->nodeKind) + make_strbuf_option("pg_autoctl", "nodekind", NULL, false, NAMEDATALEN, \ + config->nodeKind) #define OPTION_POSTGRESQL_PGDATA(config) \ - make_strbuf_option("postgresql", "pgdata", "pgdata", true, MAXPGPATH, \ - config->pgSetup.pgdata) + make_strbuf_option("postgresql", "pgdata", "pgdata", true, MAXPGPATH, \ + config->pgSetup.pgdata) #define OPTION_POSTGRESQL_PG_CTL(config) \ - make_strbuf_option("postgresql", "pg_ctl", "pgctl", false, MAXPGPATH, \ - config->pgSetup.pg_ctl) + make_strbuf_option("postgresql", "pg_ctl", "pgctl", false, MAXPGPATH, \ + config->pgSetup.pg_ctl) #define OPTION_POSTGRESQL_USERNAME(config) \ - make_strbuf_option("postgresql", "username", "username", \ - false, NAMEDATALEN, \ - config->pgSetup.username) + make_strbuf_option("postgresql", "username", "username", \ + false, NAMEDATALEN, \ + config->pgSetup.username) #define OPTION_POSTGRESQL_DBNAME(config) \ - make_strbuf_option("postgresql", "dbname", "dbname", false, NAMEDATALEN, \ - config->pgSetup.dbname) + make_strbuf_option("postgresql", "dbname", "dbname", false, NAMEDATALEN, \ + config->pgSetup.dbname) #define OPTION_POSTGRESQL_HOST(config) \ - make_strbuf_option("postgresql", "host", "pghost", \ - false, _POSIX_HOST_NAME_MAX, \ - config->pgSetup.pghost) + make_strbuf_option("postgresql", "host", "pghost", \ + false, _POSIX_HOST_NAME_MAX, \ + config->pgSetup.pghost) #define OPTION_POSTGRESQL_PORT(config) \ - make_int_option("postgresql", "port", "pgport", \ - true, &(config->pgSetup.pgport)) + make_int_option("postgresql", "port", "pgport", \ + true, &(config->pgSetup.pgport)) #define OPTION_POSTGRESQL_PROXY_PORT(config) \ - make_int_option("postgresql", "proxyport", "proxyport", \ - false, &(config->pgSetup.proxyport)) + make_int_option("postgresql", "proxyport", "proxyport", \ + false, &(config->pgSetup.proxyport)) #define OPTION_POSTGRESQL_LISTEN_ADDRESSES(config) \ - make_strbuf_option("postgresql", "listen_addresses", "listen", \ - false, MAXPGPATH, config->pgSetup.listen_addresses) + make_strbuf_option("postgresql", "listen_addresses", "listen", \ + false, MAXPGPATH, config->pgSetup.listen_addresses) #define OPTION_POSTGRESQL_AUTH_METHOD(config) \ - make_strbuf_option("postgresql", "auth_method", "auth", \ - false, MAXPGPATH, config->pgSetup.authMethod) + make_strbuf_option("postgresql", "auth_method", "auth", \ + false, MAXPGPATH, config->pgSetup.authMethod) #define OPTION_POSTGRESQL_HBA_LEVEL(config) \ - make_strbuf_option("postgresql", "hba_level", NULL, \ - false, MAXPGPATH, config->pgSetup.hbaLevelStr) + make_strbuf_option("postgresql", "hba_level", NULL, \ + false, MAXPGPATH, config->pgSetup.hbaLevelStr) #define OPTION_SSL_ACTIVE(config) \ - make_int_option_default("ssl", "active", NULL, \ - false, &(config->pgSetup.ssl.active), 0) + make_int_option_default("ssl", "active", NULL, \ + false, &(config->pgSetup.ssl.active), 0) #define OPTION_SSL_MODE(config) \ - make_strbuf_option("ssl", "sslmode", "ssl-mode", \ - false, SSL_MODE_STRLEN, config->pgSetup.ssl.sslModeStr) + make_strbuf_option("ssl", "sslmode", "ssl-mode", \ + false, SSL_MODE_STRLEN, config->pgSetup.ssl.sslModeStr) #define OPTION_SSL_CA_FILE(config) \ - make_strbuf_option("ssl", "ca_file", "ssl-ca-file", \ - false, MAXPGPATH, config->pgSetup.ssl.caFile) + make_strbuf_option("ssl", "ca_file", "ssl-ca-file", \ + false, MAXPGPATH, config->pgSetup.ssl.caFile) #define OPTION_SSL_CRL_FILE(config) \ - make_strbuf_option("ssl", "crl_file", "ssl-crl-file", \ - false, MAXPGPATH, config->pgSetup.ssl.crlFile) + make_strbuf_option("ssl", "crl_file", "ssl-crl-file", \ + false, MAXPGPATH, config->pgSetup.ssl.crlFile) #define OPTION_SSL_SERVER_CERT(config) \ - make_strbuf_option("ssl", "cert_file", "server-cert", \ - false, MAXPGPATH, config->pgSetup.ssl.serverCert) + make_strbuf_option("ssl", "cert_file", "server-cert", \ + false, MAXPGPATH, config->pgSetup.ssl.serverCert) #define OPTION_SSL_SERVER_KEY(config) \ - make_strbuf_option("ssl", "key_file", "server-key", \ - false, MAXPGPATH, config->pgSetup.ssl.serverKey) + make_strbuf_option("ssl", "key_file", "server-key", \ + false, MAXPGPATH, config->pgSetup.ssl.serverKey) #define OPTION_REPLICATION_PASSWORD(config) \ - make_strbuf_option_default("replication", "password", NULL, \ - false, MAXCONNINFO, \ - config->replication_password, \ - REPLICATION_PASSWORD_DEFAULT) + make_strbuf_option_default("replication", "password", NULL, \ + false, MAXCONNINFO, \ + config->replication_password, \ + REPLICATION_PASSWORD_DEFAULT) #define OPTION_REPLICATION_MAXIMUM_BACKUP_RATE(config) \ - make_strbuf_option_default("replication", "maximum_backup_rate", NULL, \ - false, MAXIMUM_BACKUP_RATE_LEN, \ - config->maximum_backup_rate, \ - MAXIMUM_BACKUP_RATE) + make_strbuf_option_default("replication", "maximum_backup_rate", NULL, \ + false, MAXIMUM_BACKUP_RATE_LEN, \ + config->maximum_backup_rate, \ + MAXIMUM_BACKUP_RATE) #define OPTION_REPLICATION_BACKUP_DIR(config) \ - make_strbuf_option("replication", "backup_directory", NULL, \ - false, MAXPGPATH, config->backupDirectory) + make_strbuf_option("replication", "backup_directory", NULL, \ + false, MAXPGPATH, config->backupDirectory) #define OPTION_TIMEOUT_NETWORK_PARTITION(config) \ - make_int_option_default("timeout", "network_partition_timeout", \ - NULL, false, \ - &(config->network_partition_timeout), \ - NETWORK_PARTITION_TIMEOUT) + make_int_option_default("timeout", "network_partition_timeout", \ + NULL, false, \ + &(config->network_partition_timeout), \ + NETWORK_PARTITION_TIMEOUT) #define OPTION_TIMEOUT_PREPARE_PROMOTION_CATCHUP(config) \ - make_int_option_default("timeout", "prepare_promotion_catchup", \ - NULL, \ - false, \ - &(config->prepare_promotion_catchup), \ - PREPARE_PROMOTION_CATCHUP_TIMEOUT) + make_int_option_default("timeout", "prepare_promotion_catchup", \ + NULL, \ + false, \ + &(config->prepare_promotion_catchup), \ + PREPARE_PROMOTION_CATCHUP_TIMEOUT) #define OPTION_TIMEOUT_PREPARE_PROMOTION_WALRECEIVER(config) \ - make_int_option_default("timeout", "prepare_promotion_walreceiver", \ - NULL, \ - false, \ - &(config->prepare_promotion_walreceiver), \ - PREPARE_PROMOTION_WALRECEIVER_TIMEOUT) + make_int_option_default("timeout", "prepare_promotion_walreceiver", \ + NULL, \ + false, \ + &(config->prepare_promotion_walreceiver), \ + PREPARE_PROMOTION_WALRECEIVER_TIMEOUT) #define OPTION_TIMEOUT_POSTGRESQL_RESTART_FAILURE_TIMEOUT(config) \ - make_int_option_default("timeout", "postgresql_restart_failure_timeout", \ - NULL, \ - false, \ - &(config->postgresql_restart_failure_timeout), \ - POSTGRESQL_FAILS_TO_START_TIMEOUT) + make_int_option_default("timeout", "postgresql_restart_failure_timeout", \ + NULL, \ + false, \ + &(config->postgresql_restart_failure_timeout), \ + POSTGRESQL_FAILS_TO_START_TIMEOUT) #define OPTION_TIMEOUT_POSTGRESQL_RESTART_FAILURE_MAX_RETRIES(config) \ - make_int_option_default("timeout", "postgresql_restart_failure_max_retries", \ - NULL, \ - false, \ - &(config->postgresql_restart_failure_max_retries), \ - POSTGRESQL_FAILS_TO_START_RETRIES) + make_int_option_default("timeout", "postgresql_restart_failure_max_retries", \ + NULL, \ + false, \ + &(config->postgresql_restart_failure_max_retries), \ + POSTGRESQL_FAILS_TO_START_RETRIES) #define OPTION_TIMEOUT_CITUS_MASTER_UPDATE_NODE_LOCK_COOLDOWN(config) \ - make_int_option_default("timeout", "citus_master_update_node_lock_cooldown", \ - NULL, \ - false, \ - &(config->citus_master_update_node_lock_cooldown), \ - CITUS_MASTER_UPDATE_NODE_LOCK_COOLDOWN) + make_int_option_default("timeout", "citus_master_update_node_lock_cooldown", \ + NULL, \ + false, \ + &(config->citus_master_update_node_lock_cooldown), \ + CITUS_MASTER_UPDATE_NODE_LOCK_COOLDOWN) #define OPTION_TIMEOUT_CITUS_COORDINATOR_WAIT_MAX_RETRIES(config) \ - make_int_option_default("timeout", "citus_coordinator_wait_max_retries", \ - NULL, \ - false, \ - &(config->citus_coordinator_wait_max_retries), \ - CITUS_COORDINATOR_WAIT_MAX_RETRIES) + make_int_option_default("timeout", "citus_coordinator_wait_max_retries", \ + NULL, \ + false, \ + &(config->citus_coordinator_wait_max_retries), \ + CITUS_COORDINATOR_WAIT_MAX_RETRIES) #define OPTION_TIMEOUT_CITUS_COORDINATOR_WAIT_TIMEOUT(config) \ - make_int_option_default("timeout", "citus_coordinator_wait_timeout", \ - NULL, \ - false, \ - &(config->citus_coordinator_wait_timeout), \ - CITUS_COORDINATOR_WAIT_TIMEOUT) + make_int_option_default("timeout", "citus_coordinator_wait_timeout", \ + NULL, \ + false, \ + &(config->citus_coordinator_wait_timeout), \ + CITUS_COORDINATOR_WAIT_TIMEOUT) #define OPTION_TIMEOUT_LISTEN_NOTIFICATIONS(config) \ - make_int_option_default("timeout", "listen_notifications_timeout", \ - NULL, false, \ - &(config->listen_notifications_timeout), \ - PG_AUTOCTL_LISTEN_NOTIFICATIONS_TIMEOUT) + make_int_option_default("timeout", "listen_notifications_timeout", \ + NULL, false, \ + &(config->listen_notifications_timeout), \ + PG_AUTOCTL_LISTEN_NOTIFICATIONS_TIMEOUT) #define OPTION_CITUS_ROLE(config) \ - make_strbuf_option_default("citus", "role", NULL, false, NAMEDATALEN, \ - config->citusRoleStr, DEFAULT_CITUS_ROLE) + make_strbuf_option_default("citus", "role", NULL, false, NAMEDATALEN, \ + config->citusRoleStr, DEFAULT_CITUS_ROLE) #define OPTION_CITUS_CLUSTER_NAME(config) \ - make_strbuf_option("citus", "cluster_name", "citus-cluster", \ - false, NAMEDATALEN, config->pgSetup.citusClusterName) + make_strbuf_option("citus", "cluster_name", "citus-cluster", \ + false, NAMEDATALEN, config->pgSetup.citusClusterName) #define SET_INI_OPTIONS_ARRAY(config) \ - { \ - OPTION_AUTOCTL_ROLE(config), \ - OPTION_AUTOCTL_MONITOR(config), \ - OPTION_AUTOCTL_FORMATION(config), \ - OPTION_AUTOCTL_GROUPID(config), \ - OPTION_AUTOCTL_NAME(config), \ - OPTION_AUTOCTL_HOSTNAME(config), \ - OPTION_AUTOCTL_NODENAME(config), \ - OPTION_AUTOCTL_NODEKIND(config), \ - OPTION_POSTGRESQL_PGDATA(config), \ - OPTION_POSTGRESQL_PG_CTL(config), \ - OPTION_POSTGRESQL_USERNAME(config), \ - OPTION_POSTGRESQL_DBNAME(config), \ - OPTION_POSTGRESQL_HOST(config), \ - OPTION_POSTGRESQL_PORT(config), \ - OPTION_POSTGRESQL_PROXY_PORT(config), \ - OPTION_POSTGRESQL_LISTEN_ADDRESSES(config), \ - OPTION_POSTGRESQL_AUTH_METHOD(config), \ - OPTION_POSTGRESQL_HBA_LEVEL(config), \ - OPTION_SSL_ACTIVE(config), \ - OPTION_SSL_MODE(config), \ - OPTION_SSL_CA_FILE(config), \ - OPTION_SSL_CRL_FILE(config), \ - OPTION_SSL_SERVER_CERT(config), \ - OPTION_SSL_SERVER_KEY(config), \ - OPTION_REPLICATION_MAXIMUM_BACKUP_RATE(config), \ - OPTION_REPLICATION_BACKUP_DIR(config), \ - OPTION_REPLICATION_PASSWORD(config), \ - OPTION_TIMEOUT_NETWORK_PARTITION(config), \ - OPTION_TIMEOUT_PREPARE_PROMOTION_CATCHUP(config), \ - OPTION_TIMEOUT_PREPARE_PROMOTION_WALRECEIVER(config), \ - OPTION_TIMEOUT_POSTGRESQL_RESTART_FAILURE_TIMEOUT(config), \ - OPTION_TIMEOUT_POSTGRESQL_RESTART_FAILURE_MAX_RETRIES(config), \ - OPTION_TIMEOUT_LISTEN_NOTIFICATIONS(config), \ + { \ + OPTION_AUTOCTL_ROLE(config), \ + OPTION_AUTOCTL_MONITOR(config), \ + OPTION_AUTOCTL_FORMATION(config), \ + OPTION_AUTOCTL_GROUPID(config), \ + OPTION_AUTOCTL_NAME(config), \ + OPTION_AUTOCTL_HOSTNAME(config), \ + OPTION_AUTOCTL_NODENAME(config), \ + OPTION_AUTOCTL_NODEKIND(config), \ + OPTION_POSTGRESQL_PGDATA(config), \ + OPTION_POSTGRESQL_PG_CTL(config), \ + OPTION_POSTGRESQL_USERNAME(config), \ + OPTION_POSTGRESQL_DBNAME(config), \ + OPTION_POSTGRESQL_HOST(config), \ + OPTION_POSTGRESQL_PORT(config), \ + OPTION_POSTGRESQL_PROXY_PORT(config), \ + OPTION_POSTGRESQL_LISTEN_ADDRESSES(config), \ + OPTION_POSTGRESQL_AUTH_METHOD(config), \ + OPTION_POSTGRESQL_HBA_LEVEL(config), \ + OPTION_SSL_ACTIVE(config), \ + OPTION_SSL_MODE(config), \ + OPTION_SSL_CA_FILE(config), \ + OPTION_SSL_CRL_FILE(config), \ + OPTION_SSL_SERVER_CERT(config), \ + OPTION_SSL_SERVER_KEY(config), \ + OPTION_REPLICATION_MAXIMUM_BACKUP_RATE(config), \ + OPTION_REPLICATION_BACKUP_DIR(config), \ + OPTION_REPLICATION_PASSWORD(config), \ + OPTION_TIMEOUT_NETWORK_PARTITION(config), \ + OPTION_TIMEOUT_PREPARE_PROMOTION_CATCHUP(config), \ + OPTION_TIMEOUT_PREPARE_PROMOTION_WALRECEIVER(config), \ + OPTION_TIMEOUT_POSTGRESQL_RESTART_FAILURE_TIMEOUT(config), \ + OPTION_TIMEOUT_POSTGRESQL_RESTART_FAILURE_MAX_RETRIES(config), \ + OPTION_TIMEOUT_LISTEN_NOTIFICATIONS(config), \ \ - OPTION_TIMEOUT_CITUS_MASTER_UPDATE_NODE_LOCK_COOLDOWN(config), \ - OPTION_TIMEOUT_CITUS_COORDINATOR_WAIT_MAX_RETRIES(config), \ - OPTION_TIMEOUT_CITUS_COORDINATOR_WAIT_TIMEOUT(config), \ + OPTION_TIMEOUT_CITUS_MASTER_UPDATE_NODE_LOCK_COOLDOWN(config), \ + OPTION_TIMEOUT_CITUS_COORDINATOR_WAIT_MAX_RETRIES(config), \ + OPTION_TIMEOUT_CITUS_COORDINATOR_WAIT_TIMEOUT(config), \ \ - OPTION_CITUS_ROLE(config), \ - OPTION_CITUS_CLUSTER_NAME(config), \ - INI_OPTION_LAST \ - } + OPTION_CITUS_ROLE(config), \ + OPTION_CITUS_CLUSTER_NAME(config), \ + INI_OPTION_LAST \ + } static bool keeper_config_init_nodekind(KeeperConfig *config); static bool keeper_config_init_hbalevel(KeeperConfig *config); diff --git a/src/bin/pg_autoctl/keeper_config.h b/src/bin/pg_autoctl/keeper_config.h index 94d417a2d..3826923d9 100644 --- a/src/bin/pg_autoctl/keeper_config.h +++ b/src/bin/pg_autoctl/keeper_config.h @@ -74,7 +74,7 @@ typedef struct KeeperConfig } KeeperConfig; #define PG_AUTOCTL_MONITOR_IS_DISABLED(config) \ - (strcmp(config->monitor_pguri, PG_AUTOCTL_MONITOR_DISABLED) == 0) + (strcmp(config->monitor_pguri, PG_AUTOCTL_MONITOR_DISABLED) == 0) bool keeper_config_set_pathnames_from_pgdata(ConfigFilePaths *pathnames, const char *pgdata); diff --git a/src/bin/pg_autoctl/monitor_config.c b/src/bin/pg_autoctl/monitor_config.c index 23611c6d9..f62be0d8b 100644 --- a/src/bin/pg_autoctl/monitor_config.c +++ b/src/bin/pg_autoctl/monitor_config.c @@ -22,8 +22,8 @@ #include "pgctl.h" #define OPTION_AUTOCTL_ROLE(config) \ - make_strbuf_option_default("pg_autoctl", "role", NULL, true, NAMEDATALEN, \ - config->role, MONITOR_ROLE) + make_strbuf_option_default("pg_autoctl", "role", NULL, true, NAMEDATALEN, \ + config->role, MONITOR_ROLE) /* * --hostname used to be --nodename, and we need to support transition from the @@ -33,96 +33,96 @@ * As a result HOSTNAME is marked not required and NODENAME is marked compat. */ #define OPTION_AUTOCTL_HOSTNAME(config) \ - make_strbuf_option("pg_autoctl", "hostname", "hostname", \ - false, _POSIX_HOST_NAME_MAX, config->hostname) + make_strbuf_option("pg_autoctl", "hostname", "hostname", \ + false, _POSIX_HOST_NAME_MAX, config->hostname) #define OPTION_AUTOCTL_NODENAME(config) \ - make_strbuf_compat_option("pg_autoctl", "nodename", \ - _POSIX_HOST_NAME_MAX, config->hostname) + make_strbuf_compat_option("pg_autoctl", "nodename", \ + _POSIX_HOST_NAME_MAX, config->hostname) #define OPTION_POSTGRESQL_PGDATA(config) \ - make_strbuf_option("postgresql", "pgdata", "pgdata", true, MAXPGPATH, \ - config->pgSetup.pgdata) + make_strbuf_option("postgresql", "pgdata", "pgdata", true, MAXPGPATH, \ + config->pgSetup.pgdata) #define OPTION_POSTGRESQL_PG_CTL(config) \ - make_strbuf_option("postgresql", "pg_ctl", "pgctl", false, MAXPGPATH, \ - config->pgSetup.pg_ctl) + make_strbuf_option("postgresql", "pg_ctl", "pgctl", false, MAXPGPATH, \ + config->pgSetup.pg_ctl) #define OPTION_POSTGRESQL_USERNAME(config) \ - make_strbuf_option("postgresql", "username", "username", \ - false, NAMEDATALEN, \ - config->pgSetup.username) + make_strbuf_option("postgresql", "username", "username", \ + false, NAMEDATALEN, \ + config->pgSetup.username) #define OPTION_POSTGRESQL_DBNAME(config) \ - make_strbuf_option("postgresql", "dbname", "dbname", false, NAMEDATALEN, \ - config->pgSetup.dbname) + make_strbuf_option("postgresql", "dbname", "dbname", false, NAMEDATALEN, \ + config->pgSetup.dbname) #define OPTION_POSTGRESQL_HOST(config) \ - make_strbuf_option("postgresql", "host", "pghost", \ - false, _POSIX_HOST_NAME_MAX, \ - config->pgSetup.pghost) + make_strbuf_option("postgresql", "host", "pghost", \ + false, _POSIX_HOST_NAME_MAX, \ + config->pgSetup.pghost) #define OPTION_POSTGRESQL_PORT(config) \ - make_int_option("postgresql", "port", "pgport", \ - true, &(config->pgSetup.pgport)) + make_int_option("postgresql", "port", "pgport", \ + true, &(config->pgSetup.pgport)) #define OPTION_POSTGRESQL_LISTEN_ADDRESSES(config) \ - make_strbuf_option("postgresql", "listen_addresses", "listen", \ - false, MAXPGPATH, config->pgSetup.listen_addresses) + make_strbuf_option("postgresql", "listen_addresses", "listen", \ + false, MAXPGPATH, config->pgSetup.listen_addresses) #define OPTION_POSTGRESQL_AUTH_METHOD(config) \ - make_strbuf_option("postgresql", "auth_method", "auth", \ - false, MAXPGPATH, config->pgSetup.authMethod) + make_strbuf_option("postgresql", "auth_method", "auth", \ + false, MAXPGPATH, config->pgSetup.authMethod) #define OPTION_SSL_ACTIVE(config) \ - make_int_option_default("ssl", "active", NULL, \ - false, &(config->pgSetup.ssl.active), 0) + make_int_option_default("ssl", "active", NULL, \ + false, &(config->pgSetup.ssl.active), 0) #define OPTION_SSL_MODE(config) \ - make_strbuf_option("ssl", "sslmode", "ssl-mode", \ - false, SSL_MODE_STRLEN, config->pgSetup.ssl.sslModeStr) + make_strbuf_option("ssl", "sslmode", "ssl-mode", \ + false, SSL_MODE_STRLEN, config->pgSetup.ssl.sslModeStr) #define OPTION_SSL_CA_FILE(config) \ - make_strbuf_option("ssl", "ca_file", "ssl-ca-file", \ - false, MAXPGPATH, config->pgSetup.ssl.caFile) + make_strbuf_option("ssl", "ca_file", "ssl-ca-file", \ + false, MAXPGPATH, config->pgSetup.ssl.caFile) #define OPTION_SSL_CRL_FILE(config) \ - make_strbuf_option("ssl", "crl_file", "ssl-crl-file", \ - false, MAXPGPATH, config->pgSetup.ssl.crlFile) + make_strbuf_option("ssl", "crl_file", "ssl-crl-file", \ + false, MAXPGPATH, config->pgSetup.ssl.crlFile) #define OPTION_SSL_SERVER_CERT(config) \ - make_strbuf_option("ssl", "cert_file", "server-cert", \ - false, MAXPGPATH, config->pgSetup.ssl.serverCert) + make_strbuf_option("ssl", "cert_file", "server-cert", \ + false, MAXPGPATH, config->pgSetup.ssl.serverCert) #define OPTION_SSL_SERVER_KEY(config) \ - make_strbuf_option("ssl", "key_file", "server-key", \ - false, MAXPGPATH, config->pgSetup.ssl.serverKey) + make_strbuf_option("ssl", "key_file", "server-key", \ + false, MAXPGPATH, config->pgSetup.ssl.serverKey) #define SET_INI_OPTIONS_ARRAY(config) \ - { \ - OPTION_AUTOCTL_ROLE(config), \ - OPTION_AUTOCTL_HOSTNAME(config), \ - OPTION_AUTOCTL_NODENAME(config), \ - OPTION_POSTGRESQL_PGDATA(config), \ - OPTION_POSTGRESQL_PG_CTL(config), \ - OPTION_POSTGRESQL_USERNAME(config), \ - OPTION_POSTGRESQL_DBNAME(config), \ - OPTION_POSTGRESQL_HOST(config), \ - OPTION_POSTGRESQL_PORT(config), \ - OPTION_POSTGRESQL_LISTEN_ADDRESSES(config), \ - OPTION_POSTGRESQL_AUTH_METHOD(config), \ - OPTION_SSL_MODE(config), \ - OPTION_SSL_ACTIVE(config), \ - OPTION_SSL_CA_FILE(config), \ - OPTION_SSL_CRL_FILE(config), \ - OPTION_SSL_SERVER_CERT(config), \ - OPTION_SSL_SERVER_KEY(config), \ - make_strbuf_option_default("pg_auto_failover", "autoctl_node_password", \ - NULL, false, MAXCONNINFO, \ - config->autoctl_node_password, ""), \ - INI_OPTION_LAST \ - } + { \ + OPTION_AUTOCTL_ROLE(config), \ + OPTION_AUTOCTL_HOSTNAME(config), \ + OPTION_AUTOCTL_NODENAME(config), \ + OPTION_POSTGRESQL_PGDATA(config), \ + OPTION_POSTGRESQL_PG_CTL(config), \ + OPTION_POSTGRESQL_USERNAME(config), \ + OPTION_POSTGRESQL_DBNAME(config), \ + OPTION_POSTGRESQL_HOST(config), \ + OPTION_POSTGRESQL_PORT(config), \ + OPTION_POSTGRESQL_LISTEN_ADDRESSES(config), \ + OPTION_POSTGRESQL_AUTH_METHOD(config), \ + OPTION_SSL_MODE(config), \ + OPTION_SSL_ACTIVE(config), \ + OPTION_SSL_CA_FILE(config), \ + OPTION_SSL_CRL_FILE(config), \ + OPTION_SSL_SERVER_CERT(config), \ + OPTION_SSL_SERVER_KEY(config), \ + make_strbuf_option_default("pg_auto_failover", "autoctl_node_password", \ + NULL, false, MAXCONNINFO, \ + config->autoctl_node_password, ""), \ + INI_OPTION_LAST \ + } /* diff --git a/src/bin/pg_autoctl/nodespec.c b/src/bin/pg_autoctl/nodespec.c index cad52241c..5f61e6d91 100644 --- a/src/bin/pg_autoctl/nodespec.c +++ b/src/bin/pg_autoctl/nodespec.c @@ -458,11 +458,11 @@ nodespec_create_argv(const NodeSpec *spec, int i = 0; #define PUSH(v) do { \ - if (i >= args_size - 1) { \ - log_error("nodespec_create_argv: args[] overflow"); \ - return -1; \ - } \ - args[i++] = (char *) (v); \ + if (i >= args_size - 1) { \ + log_error("nodespec_create_argv: args[] overflow"); \ + return -1; \ + } \ + args[i++] = (char *) (v); \ } while (0) PUSH(pg_autoctl_path); diff --git a/src/bin/pg_autoctl/primary_standby.c b/src/bin/pg_autoctl/primary_standby.c index af2db8a35..96a171978 100644 --- a/src/bin/pg_autoctl/primary_standby.c +++ b/src/bin/pg_autoctl/primary_standby.c @@ -39,39 +39,39 @@ static void local_postgres_update_pg_failures_tracking(LocalPostgresServer *post * replaced with dynamic values from the setup when used. */ #define DEFAULT_GUC_SETTINGS_FOR_PG_AUTO_FAILOVER \ - { "shared_preload_libraries", "pg_stat_statements" }, \ - { "listen_addresses", "'*'" }, \ - { "port", "5432" }, \ - { "max_wal_senders", "12" }, \ - { "max_replication_slots", "12" }, \ - { "wal_level", "'replica'" }, \ - { "wal_log_hints", "on" }, \ - { "wal_sender_timeout", "'30s'" }, \ - { "hot_standby_feedback", "on" }, \ - { "hot_standby", "on" }, \ - { "synchronous_commit", "on" }, \ - { "logging_collector", "on" }, \ - { "log_destination", "stderr" }, \ - { "log_directory", "log" }, \ - { "log_min_messages", "info" }, \ - { "log_connections", "off" }, \ - { "log_disconnections", "off" }, \ - { "log_lock_waits", "on" }, \ - { "password_encryption", "md5" }, \ - { "ssl", "off" }, \ - { "ssl_ca_file", "" }, \ - { "ssl_crl_file", "" }, \ - { "ssl_cert_file", "" }, \ - { "ssl_key_file", "" }, \ - { "ssl_ciphers", "'" DEFAULT_SSL_CIPHERS "'" } + { "shared_preload_libraries", "pg_stat_statements" }, \ + { "listen_addresses", "'*'" }, \ + { "port", "5432" }, \ + { "max_wal_senders", "12" }, \ + { "max_replication_slots", "12" }, \ + { "wal_level", "'replica'" }, \ + { "wal_log_hints", "on" }, \ + { "wal_sender_timeout", "'30s'" }, \ + { "hot_standby_feedback", "on" }, \ + { "hot_standby", "on" }, \ + { "synchronous_commit", "on" }, \ + { "logging_collector", "on" }, \ + { "log_destination", "stderr" }, \ + { "log_directory", "log" }, \ + { "log_min_messages", "info" }, \ + { "log_connections", "off" }, \ + { "log_disconnections", "off" }, \ + { "log_lock_waits", "on" }, \ + { "password_encryption", "md5" }, \ + { "ssl", "off" }, \ + { "ssl_ca_file", "" }, \ + { "ssl_crl_file", "" }, \ + { "ssl_cert_file", "" }, \ + { "ssl_key_file", "" }, \ + { "ssl_ciphers", "'" DEFAULT_SSL_CIPHERS "'" } #define DEFAULT_GUC_SETTINGS_FOR_PG_AUTO_FAILOVER_PRE_13 \ - DEFAULT_GUC_SETTINGS_FOR_PG_AUTO_FAILOVER, \ - { "wal_keep_segments", "512" } + DEFAULT_GUC_SETTINGS_FOR_PG_AUTO_FAILOVER, \ + { "wal_keep_segments", "512" } #define DEFAULT_GUC_SETTINGS_FOR_PG_AUTO_FAILOVER_13 \ - DEFAULT_GUC_SETTINGS_FOR_PG_AUTO_FAILOVER, \ - { "wal_keep_size", "'8 GB'" } + DEFAULT_GUC_SETTINGS_FOR_PG_AUTO_FAILOVER, \ + { "wal_keep_size", "'8 GB'" } GUC postgres_default_settings_pre_13[] = { DEFAULT_GUC_SETTINGS_FOR_PG_AUTO_FAILOVER_PRE_13, diff --git a/src/bin/pg_autoctl/systemd_config.c b/src/bin/pg_autoctl/systemd_config.c index 87bd37432..6e4de794b 100644 --- a/src/bin/pg_autoctl/systemd_config.c +++ b/src/bin/pg_autoctl/systemd_config.c @@ -24,57 +24,57 @@ #include "runprogram.h" #define OPTION_SYSTEMD_DESCRIPTION(config) \ - make_strbuf_option_default("Unit", "Description", NULL, true, BUFSIZE, \ - config->Description, "pg_auto_failover") + make_strbuf_option_default("Unit", "Description", NULL, true, BUFSIZE, \ + config->Description, "pg_auto_failover") #define OPTION_SYSTEMD_WORKING_DIRECTORY(config) \ - make_strbuf_option_default("Service", "WorkingDirectory", \ - NULL, true, BUFSIZE, \ - config->WorkingDirectory, "/var/lib/postgresql") + make_strbuf_option_default("Service", "WorkingDirectory", \ + NULL, true, BUFSIZE, \ + config->WorkingDirectory, "/var/lib/postgresql") #define OPTION_SYSTEMD_ENVIRONMENT_PGDATA(config) \ - make_strbuf_option_default("Service", "Environment", \ - NULL, true, BUFSIZE, \ - config->EnvironmentPGDATA, \ - "PGDATA=/var/lib/postgresql/11/pg_auto_failover") + make_strbuf_option_default("Service", "Environment", \ + NULL, true, BUFSIZE, \ + config->EnvironmentPGDATA, \ + "PGDATA=/var/lib/postgresql/11/pg_auto_failover") #define OPTION_SYSTEMD_USER(config) \ - make_strbuf_option_default("Service", "User", NULL, true, BUFSIZE, \ - config->User, "postgres") + make_strbuf_option_default("Service", "User", NULL, true, BUFSIZE, \ + config->User, "postgres") #define OPTION_SYSTEMD_EXECSTART(config) \ - make_strbuf_option_default("Service", "ExecStart", NULL, true, BUFSIZE, \ - config->ExecStart, "/usr/bin/pg_autoctl run") + make_strbuf_option_default("Service", "ExecStart", NULL, true, BUFSIZE, \ + config->ExecStart, "/usr/bin/pg_autoctl run") #define OPTION_SYSTEMD_RESTART(config) \ - make_strbuf_option_default("Service", "Restart", NULL, true, BUFSIZE, \ - config->Restart, "always") + make_strbuf_option_default("Service", "Restart", NULL, true, BUFSIZE, \ + config->Restart, "always") #define OPTION_SYSTEMD_STARTLIMITBURST(config) \ - make_int_option_default("Service", "StartLimitBurst", NULL, true, \ - &(config->StartLimitBurst), 20) + make_int_option_default("Service", "StartLimitBurst", NULL, true, \ + &(config->StartLimitBurst), 20) #define OPTION_SYSTEMD_EXECRELOAD(config) \ - make_strbuf_option_default("Service", "ExecReload", NULL, true, BUFSIZE, \ - config->ExecReload, "/usr/bin/pg_autoctl reload") + make_strbuf_option_default("Service", "ExecReload", NULL, true, BUFSIZE, \ + config->ExecReload, "/usr/bin/pg_autoctl reload") #define OPTION_SYSTEMD_WANTEDBY(config) \ - make_strbuf_option_default("Install", "WantedBy", NULL, true, BUFSIZE, \ - config->WantedBy, "multi-user.target") + make_strbuf_option_default("Install", "WantedBy", NULL, true, BUFSIZE, \ + config->WantedBy, "multi-user.target") #define SET_INI_OPTIONS_ARRAY(config) \ - { \ - OPTION_SYSTEMD_DESCRIPTION(config), \ - OPTION_SYSTEMD_WORKING_DIRECTORY(config), \ - OPTION_SYSTEMD_ENVIRONMENT_PGDATA(config), \ - OPTION_SYSTEMD_USER(config), \ - OPTION_SYSTEMD_EXECSTART(config), \ - OPTION_SYSTEMD_RESTART(config), \ - OPTION_SYSTEMD_STARTLIMITBURST(config), \ - OPTION_SYSTEMD_EXECRELOAD(config), \ - OPTION_SYSTEMD_WANTEDBY(config), \ - INI_OPTION_LAST \ - } + { \ + OPTION_SYSTEMD_DESCRIPTION(config), \ + OPTION_SYSTEMD_WORKING_DIRECTORY(config), \ + OPTION_SYSTEMD_ENVIRONMENT_PGDATA(config), \ + OPTION_SYSTEMD_USER(config), \ + OPTION_SYSTEMD_EXECSTART(config), \ + OPTION_SYSTEMD_RESTART(config), \ + OPTION_SYSTEMD_STARTLIMITBURST(config), \ + OPTION_SYSTEMD_EXECRELOAD(config), \ + OPTION_SYSTEMD_WANTEDBY(config), \ + INI_OPTION_LAST \ + } /* diff --git a/src/bin/pgaftest/compose_gen.c b/src/bin/pgaftest/compose_gen.c index 94524f5f4..e37a5b4b4 100644 --- a/src/bin/pgaftest/compose_gen.c +++ b/src/bin/pgaftest/compose_gen.c @@ -48,7 +48,7 @@ * The monitor hostname is always "monitor" inside the compose network. */ #define MONITOR_PGURI \ - "postgresql://autoctl_node@monitor/pg_auto_failover" + "postgresql://autoctl_node@monitor/pg_auto_failover" /* Fixed path inside every container */ #define NODE_INI_PATH "/etc/pgaf/node.ini" @@ -81,21 +81,21 @@ debian_pg_version(void) * - CA cert copied to ~/.postgresql/root.crt so libpq trusts server certs. */ #define SSL_COPY_CERTS_CMD \ - "cp " SSL_DIR_IN_CONTAINER "/server/server.crt /var/lib/postgres/server.crt" \ - " && cp " SSL_DIR_IN_CONTAINER \ - "/server/server.key /var/lib/postgres/server.key" \ - " && chmod 0600 /var/lib/postgres/server.key" \ - " && mkdir -p /var/lib/postgres/.postgresql" \ - " && cp " \ - SSL_DIR_IN_CONTAINER \ - "/client/postgresql.crt /var/lib/postgres/.postgresql/postgresql.crt" \ - " && cp " \ - SSL_DIR_IN_CONTAINER \ - "/client/postgresql.key /var/lib/postgres/.postgresql/postgresql.key" \ - " && chmod 0600 /var/lib/postgres/.postgresql/postgresql.key" \ - " && cp " \ - SSL_DIR_IN_CONTAINER "/ca.crt /var/lib/postgres/.postgresql/root.crt" \ - " &&" + "cp " SSL_DIR_IN_CONTAINER "/server/server.crt /var/lib/postgres/server.crt" \ + " && cp " SSL_DIR_IN_CONTAINER \ + "/server/server.key /var/lib/postgres/server.key" \ + " && chmod 0600 /var/lib/postgres/server.key" \ + " && mkdir -p /var/lib/postgres/.postgresql" \ + " && cp " \ + SSL_DIR_IN_CONTAINER \ + "/client/postgresql.crt /var/lib/postgres/.postgresql/postgresql.crt" \ + " && cp " \ + SSL_DIR_IN_CONTAINER \ + "/client/postgresql.key /var/lib/postgres/.postgresql/postgresql.key" \ + " && chmod 0600 /var/lib/postgres/.postgresql/postgresql.key" \ + " && cp " \ + SSL_DIR_IN_CONTAINER "/ca.crt /var/lib/postgres/.postgresql/root.crt" \ + " &&" /* @@ -520,9 +520,9 @@ compose_gen_write(TestCluster *cluster, { fformat(f, " - ./ssl/ca.crt:" SSL_DIR_IN_CONTAINER "/ca.crt:ro\n" - " - ./ssl/monitor:" + " - ./ssl/monitor:" SSL_DIR_IN_CONTAINER "/server:ro\n" - " - ./ssl/client:" + " - ./ssl/client:" SSL_DIR_IN_CONTAINER "/client:ro\n"); } if (cluster->bindSource) @@ -549,7 +549,7 @@ compose_gen_write(TestCluster *cluster, " command: [\"/bin/sh\", \"-c\"," " \"%s rm -f /tmp/pg_autoctl%s/pg_autoctl.pid" " && exec pg_autoctl node run " NODE_INI_PATH "\"]\n" - " stop_grace_period: 60s\n\n", + " stop_grace_period: 60s\n\n", ssl_needs_certs(cluster->ssl) ? SSL_COPY_CERTS_CMD : "", monitor_pgdata); } @@ -575,9 +575,9 @@ compose_gen_write(TestCluster *cluster, { fformat(f, " - ./ssl/ca.crt:" SSL_DIR_IN_CONTAINER "/ca.crt:ro\n" - " - ./ssl/%s:" + " - ./ssl/%s:" SSL_DIR_IN_CONTAINER "/server:ro\n" - " - ./ssl/client:" + " - ./ssl/client:" SSL_DIR_IN_CONTAINER "/client:ro\n", svc); } @@ -596,9 +596,9 @@ compose_gen_write(TestCluster *cluster, fformat(f, " command: [\"/bin/sh\", \"-c\"," " \"%s rm -f /tmp/pg_autoctl" NODE_PGDATA "/pg_autoctl.pid" - " && exec pg_autoctl node run " + " && exec pg_autoctl node run " NODE_INI_PATH "\"]\n" - " stop_grace_period: 60s\n\n", + " stop_grace_period: 60s\n\n", ssl_needs_certs(cluster->ssl) ? SSL_COPY_CERTS_CMD : ""); } @@ -655,9 +655,9 @@ compose_gen_write(TestCluster *cluster, { fformat(f, " - ./ssl/ca.crt:" SSL_DIR_IN_CONTAINER "/ca.crt:ro\n" - " - ./ssl/%s:" + " - ./ssl/%s:" SSL_DIR_IN_CONTAINER "/server:ro\n" - " - ./ssl/client:" + " - ./ssl/client:" SSL_DIR_IN_CONTAINER "/client:ro\n", n->name); } @@ -688,7 +688,7 @@ compose_gen_write(TestCluster *cluster, " command: [\"/bin/sh\", \"-c\"," " \"%s rm -f /tmp/pg_autoctl%s/pg_autoctl.pid" " && exec pg_autoctl node run " NODE_INI_PATH "\"]\n" - " stop_grace_period: 60s\n", + " stop_grace_period: 60s\n", ssl_needs_certs(node_ssl) ? SSL_COPY_CERTS_CMD : "", node_pgdata); @@ -910,8 +910,8 @@ compose_gen_write_monitor_ini(const TestCluster *cluster, const char *dir) * user) in the start command so the key is owned by the container * user and PostgreSQL can open it. */ - "cert_file = /var/lib/postgres/server.crt\n" - "key_file = /var/lib/postgres/server.key\n"); + "cert_file = /var/lib/postgres/server.crt\n" + "key_file = /var/lib/postgres/server.key\n"); } if (cluster->monitorPassword[0]) @@ -978,11 +978,11 @@ compose_gen_write_second_monitor_ini(const TestCluster *cluster, const char *dir "\n" "[postgresql]\n" "pgdata = " NODE_PGDATA "\n" - "\n" - "[options]\n" - "ssl = %s\n" - "auth = %s\n" - "pg_hba_lan = false\n", + "\n" + "[options]\n" + "ssl = %s\n" + "auth = %s\n" + "pg_hba_lan = false\n", name, name, cluster->ssl, cluster->auth); @@ -1000,8 +1000,8 @@ compose_gen_write_second_monitor_ini(const TestCluster *cluster, const char *dir * user) in the start command so the key is owned by the container * user and PostgreSQL can open it. */ - "cert_file = /var/lib/postgres/server.crt\n" - "key_file = /var/lib/postgres/server.key\n"); + "cert_file = /var/lib/postgres/server.crt\n" + "key_file = /var/lib/postgres/server.key\n"); } fclose(f); @@ -1085,7 +1085,7 @@ compose_gen_write_node_ini(const TestCluster *cluster, "\n" "[postgresql]\n" "pgdata = " DEBIAN_PGDATA_PREFIX "/%s/%s\n" - "\n", + "\n", debian_pg_version(), node->debianCluster); } else @@ -1094,7 +1094,7 @@ compose_gen_write_node_ini(const TestCluster *cluster, "\n" "[postgresql]\n" "pgdata = " NODE_PGDATA "\n" - "\n"); + "\n"); } if (node->noMonitor) @@ -1119,7 +1119,7 @@ compose_gen_write_node_ini(const TestCluster *cluster, fformat(f, "[monitor]\n" "pguri = " MONITOR_PGURI "\n" - "\n"); + "\n"); } fformat(f, @@ -1172,8 +1172,8 @@ compose_gen_write_node_ini(const TestCluster *cluster, * user) in the start command so the key is owned by the container * user and PostgreSQL can open it. */ - "cert_file = /var/lib/postgres/server.crt\n" - "key_file = /var/lib/postgres/server.key\n"); + "cert_file = /var/lib/postgres/server.crt\n" + "key_file = /var/lib/postgres/server.key\n"); } if (node->listen) diff --git a/src/bin/pgaftest/test_runner.c b/src/bin/pgaftest/test_runner.c index 5803dcf44..c0f528c34 100644 --- a/src/bin/pgaftest/test_runner.c +++ b/src/bin/pgaftest/test_runner.c @@ -1219,7 +1219,7 @@ wait_for_states(TestRunner *r, TestCmd *cmd) time_t t0 = time(NULL); if (!monitor_wait_formation_states(r, - (const char (*)[64]) cmd->waitStates, + (const char (*)[64])cmd->waitStates, cmd->waitStateCount, cmd->waitGroups, cmd->waitGroupCount, @@ -2176,7 +2176,7 @@ runner_exec_cmd(TestRunner *r, TestCmd *cmd, char *errBuf, int errLen) bool seenThrough[PGAF_MAX_WAIT_STATES] = { false }; if (!runner_wait_notify_goal(r, cmd->service, cmd->state, - (const char (*)[64]) cmd->passThroughStates, + (const char (*)[64])cmd->passThroughStates, seenThrough, cmd->passThroughCount, cmd->timeoutSeconds)) { diff --git a/src/bin/pgaftest/test_spec_parse.c b/src/bin/pgaftest/test_spec_parse.c index bc41648e1..2595cab42 100644 --- a/src/bin/pgaftest/test_spec_parse.c +++ b/src/bin/pgaftest/test_spec_parse.c @@ -326,7 +326,7 @@ static void yyerror(const char *msg) { fprintf(/* IGNORE-BANNED */ stderr, "pgaftest: parse error at line %d: %s\n", - pgaf_line_number, msg); + pgaf_line_number, msg); exit(1); } @@ -703,24 +703,24 @@ union yyalloc /* The size of an array large to enough to hold all stacks, each with * N elements. */ # define YYSTACK_BYTES(N) \ - ((N) *(sizeof(yytype_int16) + sizeof(YYSTYPE)) \ - + YYSTACK_GAP_MAXIMUM) + ((N) *(sizeof(yytype_int16) + sizeof(YYSTYPE)) \ + + YYSTACK_GAP_MAXIMUM) /* Copy COUNT objects from FROM to TO. The source and destination do * not overlap. */ # ifndef YYCOPY # if defined __GNUC__ && 1 < __GNUC__ # define YYCOPY(To, From, Count) \ - __builtin_memcpy(To, From, (Count) * sizeof(*(From))) + __builtin_memcpy(To, From, (Count) * sizeof(*(From))) # else # define YYCOPY(To, From, Count) \ - do \ - { \ - YYSIZE_T yyi; \ - for (yyi = 0; yyi < (Count); yyi++) { \ - (To)[yyi] = (From)[yyi]; } \ - } \ - while (YYID(0)) + do \ + { \ + YYSIZE_T yyi; \ + for (yyi = 0; yyi < (Count); yyi++) { \ + (To)[yyi] = (From)[yyi]; } \ + } \ + while (YYID(0)) # endif # endif @@ -730,15 +730,15 @@ union yyalloc * stack. Advance YYPTR to a properly aligned location for the next * stack. */ # define YYSTACK_RELOCATE(Stack) \ - do \ - { \ - YYSIZE_T yynewbytes; \ - YYCOPY(&yyptr->Stack, Stack, yysize); \ - Stack = &yyptr->Stack; \ - yynewbytes = yystacksize * sizeof(*Stack) + YYSTACK_GAP_MAXIMUM; \ - yyptr += yynewbytes / sizeof(*yyptr); \ - } \ - while (YYID(0)) + do \ + { \ + YYSIZE_T yynewbytes; \ + YYCOPY(&yyptr->Stack, Stack, yysize); \ + Stack = &yyptr->Stack; \ + yynewbytes = yystacksize * sizeof(*Stack) + YYSTACK_GAP_MAXIMUM; \ + yyptr += yynewbytes / sizeof(*yyptr); \ + } \ + while (YYID(0)) #endif @@ -765,7 +765,7 @@ union yyalloc #define YYMAXUTOK 362 #define YYTRANSLATE(YYX) \ - ((unsigned int) (YYX) <= YYMAXUTOK ? yytranslate[YYX] : YYUNDEFTOK) + ((unsigned int) (YYX) <= YYMAXUTOK ? yytranslate[YYX] : YYUNDEFTOK) /* YYTRANSLATE[YYLEX] -- Bison symbol number corresponding to YYLEX. */ static const yytype_uint8 yytranslate[] = @@ -1322,7 +1322,7 @@ static const yytype_uint8 yystos[] = #define YYRECOVERING() (!!yyerrstatus) #define YYBACKUP(Token, Value) \ - do \ + do \ if (yychar == YYEMPTY && yylen == 1) \ { \ yychar = (Token); \ @@ -1336,7 +1336,7 @@ static const yytype_uint8 yystos[] = yyerror(YY_("syntax error: cannot back up")); \ YYERROR; \ } \ - while (YYID(0)) + while (YYID(0)) #define YYTERROR 1 @@ -1350,7 +1350,7 @@ static const yytype_uint8 yystos[] = #define YYRHSLOC(Rhs, K) ((Rhs)[K]) #ifndef YYLLOC_DEFAULT # define YYLLOC_DEFAULT(Current, Rhs, N) \ - do \ + do \ if (YYID(N)) \ { \ (Current).first_line = YYRHSLOC(Rhs, 1).first_line; \ @@ -1365,7 +1365,7 @@ static const yytype_uint8 yystos[] = (Current).first_column = (Current).last_column = \ YYRHSLOC(Rhs, 0).last_column; \ } \ - while (YYID(0)) + while (YYID(0)) #endif @@ -1376,9 +1376,9 @@ static const yytype_uint8 yystos[] = #ifndef YY_LOCATION_PRINT # if defined YYLTYPE_IS_TRIVIAL && YYLTYPE_IS_TRIVIAL # define YY_LOCATION_PRINT(File, Loc) \ - fprintf(/* IGNORE-BANNED */ File, "%d.%d-%d.%d", \ - (Loc).first_line, (Loc).first_column, \ - (Loc).last_line, (Loc).last_column) + fprintf(/* IGNORE-BANNED */ File, "%d.%d-%d.%d", \ + (Loc).first_line, (Loc).first_column, \ + (Loc).last_line, (Loc).last_column) # else # define YY_LOCATION_PRINT(File, Loc) ((void) 0) # endif @@ -1402,21 +1402,21 @@ static const yytype_uint8 yystos[] = # endif # define YYDPRINTF(Args) \ - do { \ - if (yydebug) { \ - YYFPRINTF Args; } \ - } while (YYID(0)) + do { \ + if (yydebug) { \ + YYFPRINTF Args; } \ + } while (YYID(0)) # define YY_SYMBOL_PRINT(Title, Type, Value, Location) \ - do { \ - if (yydebug) \ - { \ - YYFPRINTF(stderr, "%s ", Title); \ - yy_symbol_print(stderr, \ - Type, Value); \ - YYFPRINTF(stderr, "\n"); \ - } \ - } while (YYID(0)) + do { \ + if (yydebug) \ + { \ + YYFPRINTF(stderr, "%s ", Title); \ + yy_symbol_print(stderr, \ + Type, Value); \ + YYFPRINTF(stderr, "\n"); \ + } \ + } while (YYID(0)) /*--------------------------------. @@ -1507,10 +1507,10 @@ yytype_int16 *top; } # define YY_STACK_PRINT(Bottom, Top) \ - do { \ - if (yydebug) { \ - yy_stack_print((Bottom), (Top)); } \ - } while (YYID(0)) + do { \ + if (yydebug) { \ + yy_stack_print((Bottom), (Top)); } \ + } while (YYID(0)) /*------------------------------------------------. @@ -1544,10 +1544,10 @@ int yyrule; } # define YY_REDUCE_PRINT(Rule) \ - do { \ - if (yydebug) { \ - yy_reduce_print(yyvsp, Rule); } \ - } while (YYID(0)) + do { \ + if (yydebug) { \ + yy_reduce_print(yyvsp, Rule); } \ + } while (YYID(0)) /* Nonzero means print parse trace. It is left uninitialized so that * multiple parsers can coexist. */ @@ -2183,1570 +2183,1570 @@ yyparse() { case 9: #line 233 "test_spec_parse.y" - { - strlcpy(current_spec->cluster.ssl, "self-signed", - sizeof(current_spec->cluster.ssl)); - strlcpy(current_spec->cluster.auth, "trust", - sizeof(current_spec->cluster.auth)); - break; - } + { + strlcpy(current_spec->cluster.ssl, "self-signed", + sizeof(current_spec->cluster.ssl)); + strlcpy(current_spec->cluster.auth, "trust", + sizeof(current_spec->cluster.auth)); + break; + } case 19: #line 254 "test_spec_parse.y" - { - current_spec->cluster.bindSource = true; - break; - } + { + current_spec->cluster.bindSource = true; + break; + } case 20: #line 268 "test_spec_parse.y" - { - current_spec->cluster.withMonitor = true; - break; - } + { + current_spec->cluster.withMonitor = true; + break; + } case 21: #line 272 "test_spec_parse.y" - { - current_spec->cluster.withMonitor = true; - strlcpy(current_spec->cluster.monitorDebianCluster, (yyvsp[(3) - - (3)].str), - sizeof(current_spec->cluster.monitorDebianCluster)); - free((yyvsp[(3) - (3)].str)); - break; - } + { + current_spec->cluster.withMonitor = true; + strlcpy(current_spec->cluster.monitorDebianCluster, (yyvsp[(3) - + (3)].str), + sizeof(current_spec->cluster.monitorDebianCluster)); + free((yyvsp[(3) - (3)].str)); + break; + } case 22: #line 279 "test_spec_parse.y" - { - current_spec->cluster.withMonitor = true; - strlcpy(current_spec->cluster.monitorImageTarget, (yyvsp[(3) - (3)].str), - sizeof(current_spec->cluster.monitorImageTarget)); - free((yyvsp[(3) - (3)].str)); - break; - } + { + current_spec->cluster.withMonitor = true; + strlcpy(current_spec->cluster.monitorImageTarget, (yyvsp[(3) - (3)].str), + sizeof(current_spec->cluster.monitorImageTarget)); + free((yyvsp[(3) - (3)].str)); + break; + } case 23: #line 286 "test_spec_parse.y" - { - current_spec->cluster.withMonitor = true; + { + current_spec->cluster.withMonitor = true; - /* monitor port not stored in TestCluster yet; ignore */ - (void) (yyvsp[(3) - (3)].ival); - break; - } + /* monitor port not stored in TestCluster yet; ignore */ + (void) (yyvsp[(3) - (3)].ival); + break; + } case 24: #line 292 "test_spec_parse.y" - { - current_spec->cluster.withMonitor = true; - strlcpy(current_spec->cluster.monitorPassword, (yyvsp[(3) - (3)].str), - sizeof(current_spec->cluster.monitorPassword)); - free((yyvsp[(3) - (3)].str)); - break; - } + { + current_spec->cluster.withMonitor = true; + strlcpy(current_spec->cluster.monitorPassword, (yyvsp[(3) - (3)].str), + sizeof(current_spec->cluster.monitorPassword)); + free((yyvsp[(3) - (3)].str)); + break; + } case 25: #line 299 "test_spec_parse.y" - { - strlcpy(current_spec->cluster.secondMonitorName, (yyvsp[(2) - (4)].str), - sizeof(current_spec->cluster.secondMonitorName)); - current_spec->cluster.secondMonitorStopped = true; - free((yyvsp[(2) - (4)].str)); - break; - } + { + strlcpy(current_spec->cluster.secondMonitorName, (yyvsp[(2) - (4)].str), + sizeof(current_spec->cluster.secondMonitorName)); + current_spec->cluster.secondMonitorStopped = true; + free((yyvsp[(2) - (4)].str)); + break; + } case 26: #line 306 "test_spec_parse.y" - { - strlcpy(current_spec->cluster.secondMonitorName, (yyvsp[(2) - (4)].str), - sizeof(current_spec->cluster.secondMonitorName)); - current_spec->cluster.secondMonitorStopped = true; - free((yyvsp[(2) - (4)].str)); - break; - } + { + strlcpy(current_spec->cluster.secondMonitorName, (yyvsp[(2) - (4)].str), + sizeof(current_spec->cluster.secondMonitorName)); + current_spec->cluster.secondMonitorStopped = true; + free((yyvsp[(2) - (4)].str)); + break; + } case 27: #line 313 "test_spec_parse.y" - { - strlcpy(current_spec->cluster.secondMonitorName, (yyvsp[(2) - (6)].str), - sizeof(current_spec->cluster.secondMonitorName)); - current_spec->cluster.secondMonitorStopped = true; - free((yyvsp[(2) - (6)].str)); - - /* password for second monitor not yet stored */ - free((yyvsp[(6) - (6)].str)); - break; - } + { + strlcpy(current_spec->cluster.secondMonitorName, (yyvsp[(2) - (6)].str), + sizeof(current_spec->cluster.secondMonitorName)); + current_spec->cluster.secondMonitorStopped = true; + free((yyvsp[(2) - (6)].str)); + + /* password for second monitor not yet stored */ + free((yyvsp[(6) - (6)].str)); + break; + } case 28: #line 326 "test_spec_parse.y" - { - strlcpy(current_spec->cluster.image, (yyvsp[(2) - (2)].str), - sizeof(current_spec->cluster.image)); - free((yyvsp[(2) - (2)].str)); - break; - } + { + strlcpy(current_spec->cluster.image, (yyvsp[(2) - (2)].str), + sizeof(current_spec->cluster.image)); + free((yyvsp[(2) - (2)].str)); + break; + } case 29: #line 332 "test_spec_parse.y" - { - strlcpy(current_spec->cluster.image, (yyvsp[(2) - (2)].str), - sizeof(current_spec->cluster.image)); - free((yyvsp[(2) - (2)].str)); - break; - } + { + strlcpy(current_spec->cluster.image, (yyvsp[(2) - (2)].str), + sizeof(current_spec->cluster.image)); + free((yyvsp[(2) - (2)].str)); + break; + } case 30: #line 342 "test_spec_parse.y" - { - strlcpy(current_spec->cluster.extensionVersion, (yyvsp[(2) - (2)].str), - sizeof(current_spec->cluster.extensionVersion)); - free((yyvsp[(2) - (2)].str)); - break; - } + { + strlcpy(current_spec->cluster.extensionVersion, (yyvsp[(2) - (2)].str), + sizeof(current_spec->cluster.extensionVersion)); + free((yyvsp[(2) - (2)].str)); + break; + } case 31: #line 348 "test_spec_parse.y" - { - strlcpy(current_spec->cluster.extensionVersion, (yyvsp[(2) - (2)].str), - sizeof(current_spec->cluster.extensionVersion)); - free((yyvsp[(2) - (2)].str)); - break; - } + { + strlcpy(current_spec->cluster.extensionVersion, (yyvsp[(2) - (2)].str), + sizeof(current_spec->cluster.extensionVersion)); + free((yyvsp[(2) - (2)].str)); + break; + } case 32: #line 358 "test_spec_parse.y" - { - strlcpy(current_spec->cluster.ssl, (yyvsp[(2) - (2)].str), - sizeof(current_spec->cluster.ssl)); - free((yyvsp[(2) - (2)].str)); - break; - } + { + strlcpy(current_spec->cluster.ssl, (yyvsp[(2) - (2)].str), + sizeof(current_spec->cluster.ssl)); + free((yyvsp[(2) - (2)].str)); + break; + } case 33: #line 368 "test_spec_parse.y" - { - strlcpy(current_spec->cluster.auth, (yyvsp[(2) - (2)].str), - sizeof(current_spec->cluster.auth)); - free((yyvsp[(2) - (2)].str)); - break; - } + { + strlcpy(current_spec->cluster.auth, (yyvsp[(2) - (2)].str), + sizeof(current_spec->cluster.auth)); + free((yyvsp[(2) - (2)].str)); + break; + } case 34: #line 374 "test_spec_parse.y" - { - strlcpy(current_spec->cluster.auth, (yyvsp[(2) - (2)].str), - sizeof(current_spec->cluster.auth)); - free((yyvsp[(2) - (2)].str)); - break; - } + { + strlcpy(current_spec->cluster.auth, (yyvsp[(2) - (2)].str), + sizeof(current_spec->cluster.auth)); + free((yyvsp[(2) - (2)].str)); + break; + } case 35: #line 384 "test_spec_parse.y" - { - TestCluster *cl = ¤t_spec->cluster; - if (cl->formationCount >= PGAF_MAX_FORMATIONS) - { - fprintf(/* IGNORE-BANNED */ stderr, - "pgaftest: too many formations (max %d)\n", - PGAF_MAX_FORMATIONS); - exit(1); - } - current_formation = &cl->formations[cl->formationCount++]; - strlcpy(current_formation->name, "default", - sizeof(current_formation->name)); - current_formation->numSync = -1; - break; - } + { + TestCluster *cl = ¤t_spec->cluster; + if (cl->formationCount >= PGAF_MAX_FORMATIONS) + { + fprintf(/* IGNORE-BANNED */ stderr, + "pgaftest: too many formations (max %d)\n", + PGAF_MAX_FORMATIONS); + exit(1); + } + current_formation = &cl->formations[cl->formationCount++]; + strlcpy(current_formation->name, "default", + sizeof(current_formation->name)); + current_formation->numSync = -1; + break; + } case 39: #line 411 "test_spec_parse.y" - { - (yyval.str) = (yyvsp[(1) - (1)].str); - break; - } + { + (yyval.str) = (yyvsp[(1) - (1)].str); + break; + } case 40: #line 412 "test_spec_parse.y" - { - (yyval.str) = (yyvsp[(1) - (1)].str); - break; - } + { + (yyval.str) = (yyvsp[(1) - (1)].str); + break; + } case 41: #line 413 "test_spec_parse.y" - { - (yyval.str) = strdup("auth"); - break; - } + { + (yyval.str) = strdup("auth"); + break; + } case 42: #line 414 "test_spec_parse.y" - { - (yyval.str) = strdup("monitor"); - break; - } + { + (yyval.str) = strdup("monitor"); + break; + } case 43: #line 415 "test_spec_parse.y" - { - (yyval.str) = strdup("node"); - break; - } + { + (yyval.str) = strdup("node"); + break; + } case 44: #line 420 "test_spec_parse.y" - { - strlcpy(current_formation->name, (yyvsp[(1) - (1)].str), - sizeof(current_formation->name)); - free((yyvsp[(1) - (1)].str)); - break; - } + { + strlcpy(current_formation->name, (yyvsp[(1) - (1)].str), + sizeof(current_formation->name)); + free((yyvsp[(1) - (1)].str)); + break; + } case 45: #line 425 "test_spec_parse.y" - { - current_formation->numSync = (yyvsp[(2) - (2)].ival); - break; - } + { + current_formation->numSync = (yyvsp[(2) - (2)].ival); + break; + } case 48: #line 451 "test_spec_parse.y" - { - (yyval.str) = (yyvsp[(1) - (1)].str); - break; - } + { + (yyval.str) = (yyvsp[(1) - (1)].str); + break; + } case 49: #line 452 "test_spec_parse.y" - { - (yyval.str) = strdup("monitor"); - break; - } + { + (yyval.str) = strdup("monitor"); + break; + } case 50: #line 461 "test_spec_parse.y" - { - if (current_formation->nodeCount >= PGAF_MAX_NODES) { - fprintf(/* IGNORE-BANNED */ stderr, - "pgaftest: too many nodes in formation (max %d)\n", - PGAF_MAX_NODES); - exit(1); + if (current_formation->nodeCount >= PGAF_MAX_NODES) + { + fprintf(/* IGNORE-BANNED */ stderr, + "pgaftest: too many nodes in formation (max %d)\n", + PGAF_MAX_NODES); + exit(1); + } + current_node = ¤t_formation->nodes[current_formation->nodeCount++]; + current_node->kind = NODE_KIND_STANDALONE; + current_node->candidatePriority = 50; + current_node->replicationQuorum = true; + break; } - current_node = ¤t_formation->nodes[current_formation->nodeCount++]; - current_node->kind = NODE_KIND_STANDALONE; - current_node->candidatePriority = 50; - current_node->replicationQuorum = true; - break; - } case 51: #line 478 "test_spec_parse.y" - { - strlcpy(current_node->name, (yyvsp[(1) - (2)].str), - sizeof(current_node->name)); - free((yyvsp[(1) - (2)].str)); - break; - } + { + strlcpy(current_node->name, (yyvsp[(1) - (2)].str), + sizeof(current_node->name)); + free((yyvsp[(1) - (2)].str)); + break; + } case 53: #line 485 "test_spec_parse.y" - { - strlcpy(current_node->name, (yyvsp[(2) - (3)].str), - sizeof(current_node->name)); - free((yyvsp[(2) - (3)].str)); - break; - } + { + strlcpy(current_node->name, (yyvsp[(2) - (3)].str), + sizeof(current_node->name)); + free((yyvsp[(2) - (3)].str)); + break; + } case 57: #line 499 "test_spec_parse.y" - { - current_node->kind = NODE_KIND_CITUS_COORDINATOR; - current_spec->cluster.withCitus = true; - break; - } + { + current_node->kind = NODE_KIND_CITUS_COORDINATOR; + current_spec->cluster.withCitus = true; + break; + } case 58: #line 504 "test_spec_parse.y" - { - current_node->kind = NODE_KIND_CITUS_WORKER; - current_spec->cluster.withCitus = true; - break; - } + { + current_node->kind = NODE_KIND_CITUS_WORKER; + current_spec->cluster.withCitus = true; + break; + } case 59: #line 509 "test_spec_parse.y" - { - current_node->replicationQuorum = false; - break; - } + { + current_node->replicationQuorum = false; + break; + } case 60: #line 513 "test_spec_parse.y" - { - current_node->noMonitor = true; - break; - } + { + current_node->noMonitor = true; + break; + } case 61: #line 517 "test_spec_parse.y" - { - current_node->launchDeferred = true; - break; - } + { + current_node->launchDeferred = true; + break; + } case 62: #line 521 "test_spec_parse.y" - { - current_node->launchDeferred = true; - break; - } + { + current_node->launchDeferred = true; + break; + } case 63: #line 525 "test_spec_parse.y" - { - current_node->launchDeferred = false; - break; - } + { + current_node->launchDeferred = false; + break; + } case 64: #line 529 "test_spec_parse.y" - { - current_node->launchDeferred = false; - break; - } + { + current_node->launchDeferred = false; + break; + } case 65: #line 533 "test_spec_parse.y" - { - current_node->listen = true; - break; - } + { + current_node->listen = true; + break; + } case 66: #line 537 "test_spec_parse.y" - { - current_node->citusSecondary = true; - break; - } + { + current_node->citusSecondary = true; + break; + } case 67: #line 541 "test_spec_parse.y" - { - current_node->candidatePriority = (yyvsp[(2) - (2)].ival); - break; - } + { + current_node->candidatePriority = (yyvsp[(2) - (2)].ival); + break; + } case 68: #line 545 "test_spec_parse.y" - { - current_node->group = (yyvsp[(2) - (2)].ival); - break; - } + { + current_node->group = (yyvsp[(2) - (2)].ival); + break; + } case 69: #line 549 "test_spec_parse.y" - { - current_node->pgPort = (yyvsp[(2) - (2)].ival); - break; - } + { + current_node->pgPort = (yyvsp[(2) - (2)].ival); + break; + } case 70: #line 553 "test_spec_parse.y" - { - strlcpy(current_node->citusClusterName, (yyvsp[(2) - (2)].str), - sizeof(current_node->citusClusterName)); - free((yyvsp[(2) - (2)].str)); - break; - } + { + strlcpy(current_node->citusClusterName, (yyvsp[(2) - (2)].str), + sizeof(current_node->citusClusterName)); + free((yyvsp[(2) - (2)].str)); + break; + } case 71: #line 559 "test_spec_parse.y" - { - strlcpy(current_node->debianCluster, (yyvsp[(2) - (2)].str), - sizeof(current_node->debianCluster)); - free((yyvsp[(2) - (2)].str)); - break; - } + { + strlcpy(current_node->debianCluster, (yyvsp[(2) - (2)].str), + sizeof(current_node->debianCluster)); + free((yyvsp[(2) - (2)].str)); + break; + } case 72: #line 565 "test_spec_parse.y" - { - strlcpy(current_node->ssl, (yyvsp[(2) - (2)].str), - sizeof(current_node->ssl)); - free((yyvsp[(2) - (2)].str)); - break; - } + { + strlcpy(current_node->ssl, (yyvsp[(2) - (2)].str), + sizeof(current_node->ssl)); + free((yyvsp[(2) - (2)].str)); + break; + } case 73: #line 570 "test_spec_parse.y" - { - strlcpy(current_node->auth, (yyvsp[(2) - (2)].str), - sizeof(current_node->auth)); - free((yyvsp[(2) - (2)].str)); - break; - } + { + strlcpy(current_node->auth, (yyvsp[(2) - (2)].str), + sizeof(current_node->auth)); + free((yyvsp[(2) - (2)].str)); + break; + } case 74: #line 575 "test_spec_parse.y" - { - strlcpy(current_node->auth, (yyvsp[(2) - (2)].str), - sizeof(current_node->auth)); - free((yyvsp[(2) - (2)].str)); - break; - } + { + strlcpy(current_node->auth, (yyvsp[(2) - (2)].str), + sizeof(current_node->auth)); + free((yyvsp[(2) - (2)].str)); + break; + } case 75: #line 580 "test_spec_parse.y" - { - if (strcmp((yyvsp[(2) - (2)].str), "false") == 0 || strcmp((yyvsp[(2) - - (2)].str), - "0") == 0) { - current_node->replicationQuorum = false; - } - else - { - current_node->replicationQuorum = true; + if (strcmp((yyvsp[(2) - (2)].str), "false") == 0 || strcmp((yyvsp[(2) - + (2)].str), + "0") == 0) + { + current_node->replicationQuorum = false; + } + else + { + current_node->replicationQuorum = true; + } + free((yyvsp[(2) - (2)].str)); + break; } - free((yyvsp[(2) - (2)].str)); - break; - } case 76: #line 588 "test_spec_parse.y" - { - strlcpy(current_node->replicationPassword, (yyvsp[(2) - (2)].str), - sizeof(current_node->replicationPassword)); - free((yyvsp[(2) - (2)].str)); - break; - } + { + strlcpy(current_node->replicationPassword, (yyvsp[(2) - (2)].str), + sizeof(current_node->replicationPassword)); + free((yyvsp[(2) - (2)].str)); + break; + } case 77: #line 594 "test_spec_parse.y" - { - strlcpy(current_node->monitorPassword, (yyvsp[(2) - (2)].str), - sizeof(current_node->monitorPassword)); - free((yyvsp[(2) - (2)].str)); - break; - } + { + strlcpy(current_node->monitorPassword, (yyvsp[(2) - (2)].str), + sizeof(current_node->monitorPassword)); + free((yyvsp[(2) - (2)].str)); + break; + } case 78: #line 600 "test_spec_parse.y" - { - /* volume — adds a named Docker volume */ - int vi = current_node->volumeCount; - if (vi < PGAF_MAX_NODE_VOLUMES) - { - strlcpy(current_node->volumes[vi].name, (yyvsp[(2) - (3)].str), - sizeof(current_node->volumes[0].name)); - strlcpy(current_node->volumes[vi].path, (yyvsp[(3) - (3)].str), - sizeof(current_node->volumes[0].path)); - current_node->volumeCount++; - } - free((yyvsp[(2) - (3)].str)); - free((yyvsp[(3) - (3)].str)); - break; - } + { + /* volume — adds a named Docker volume */ + int vi = current_node->volumeCount; + if (vi < PGAF_MAX_NODE_VOLUMES) + { + strlcpy(current_node->volumes[vi].name, (yyvsp[(2) - (3)].str), + sizeof(current_node->volumes[0].name)); + strlcpy(current_node->volumes[vi].path, (yyvsp[(3) - (3)].str), + sizeof(current_node->volumes[0].path)); + current_node->volumeCount++; + } + free((yyvsp[(2) - (3)].str)); + free((yyvsp[(3) - (3)].str)); + break; + } case 79: #line 614 "test_spec_parse.y" - { - /* volume "/path/with spaces" */ - int vi = current_node->volumeCount; - if (vi < PGAF_MAX_NODE_VOLUMES) - { - strlcpy(current_node->volumes[vi].name, (yyvsp[(2) - (3)].str), - sizeof(current_node->volumes[0].name)); - strlcpy(current_node->volumes[vi].path, (yyvsp[(3) - (3)].str), - sizeof(current_node->volumes[0].path)); - current_node->volumeCount++; - } - free((yyvsp[(2) - (3)].str)); - free((yyvsp[(3) - (3)].str)); - break; - } + { + /* volume "/path/with spaces" */ + int vi = current_node->volumeCount; + if (vi < PGAF_MAX_NODE_VOLUMES) + { + strlcpy(current_node->volumes[vi].name, (yyvsp[(2) - (3)].str), + sizeof(current_node->volumes[0].name)); + strlcpy(current_node->volumes[vi].path, (yyvsp[(3) - (3)].str), + sizeof(current_node->volumes[0].path)); + current_node->volumeCount++; + } + free((yyvsp[(2) - (3)].str)); + free((yyvsp[(3) - (3)].str)); + break; + } case 80: #line 635 "test_spec_parse.y" - { - current_spec->setup = (yyvsp[(2) - (2)].step); - break; - } + { + current_spec->setup = (yyvsp[(2) - (2)].step); + break; + } case 81: #line 642 "test_spec_parse.y" - { - current_spec->teardown = (yyvsp[(2) - (2)].step); - break; - } + { + current_spec->teardown = (yyvsp[(2) - (2)].step); + break; + } case 82: #line 653 "test_spec_parse.y" - { - TestStep *s = (yyvsp[(3) - (3)].step); - strlcpy(s->name, (yyvsp[(2) - (3)].str), sizeof(s->name)); - free((yyvsp[(2) - (3)].str)); - register_step(current_spec, s); - break; - } + { + TestStep *s = (yyvsp[(3) - (3)].step); + strlcpy(s->name, (yyvsp[(2) - (3)].str), sizeof(s->name)); + free((yyvsp[(2) - (3)].str)); + register_step(current_spec, s); + break; + } case 83: #line 671 "test_spec_parse.y" - { - /* post-process: CMD_SQL immediately before CMD_EXPECT_ERROR */ - for (TestCmd *c = (yyvsp[(2) - (3)].step)->commands; c; c = c->next) { - if (c->kind == CMD_SQL && c->next && - c->next->kind == CMD_EXPECT_ERROR) + /* post-process: CMD_SQL immediately before CMD_EXPECT_ERROR */ + for (TestCmd *c = (yyvsp[(2) - (3)].step)->commands; c; c = c->next) { - c->allowError = true; + if (c->kind == CMD_SQL && c->next && + c->next->kind == CMD_EXPECT_ERROR) + { + c->allowError = true; + } } + (yyval.step) = (yyvsp[(2) - (3)].step); + break; } - (yyval.step) = (yyvsp[(2) - (3)].step); - break; - } case 84: #line 685 "test_spec_parse.y" - { - (yyval.step) = make_step(""); - break; - } + { + (yyval.step) = make_step(""); + break; + } case 85: #line 689 "test_spec_parse.y" - { - if ((yyvsp[(2) - (2)].cmd)) { - append_cmd((yyvsp[(1) - (2)].step), (yyvsp[(2) - (2)].cmd)); + if ((yyvsp[(2) - (2)].cmd)) + { + append_cmd((yyvsp[(1) - (2)].step), (yyvsp[(2) - (2)].cmd)); + } + (yyval.step) = (yyvsp[(1) - (2)].step); + break; } - (yyval.step) = (yyvsp[(1) - (2)].step); - break; - } case 86: #line 696 "test_spec_parse.y" - { - (yyval.cmd) = (yyvsp[(1) - (1)].cmd); - break; - } + { + (yyval.cmd) = (yyvsp[(1) - (1)].cmd); + break; + } case 87: #line 697 "test_spec_parse.y" - { - (yyval.cmd) = (yyvsp[(1) - (1)].cmd); - break; - } + { + (yyval.cmd) = (yyvsp[(1) - (1)].cmd); + break; + } case 88: #line 698 "test_spec_parse.y" - { - (yyval.cmd) = (yyvsp[(1) - (1)].cmd); - break; - } + { + (yyval.cmd) = (yyvsp[(1) - (1)].cmd); + break; + } case 89: #line 699 "test_spec_parse.y" - { - (yyval.cmd) = (yyvsp[(1) - (1)].cmd); - break; - } + { + (yyval.cmd) = (yyvsp[(1) - (1)].cmd); + break; + } case 90: #line 700 "test_spec_parse.y" - { - (yyval.cmd) = (yyvsp[(1) - (1)].cmd); - break; - } + { + (yyval.cmd) = (yyvsp[(1) - (1)].cmd); + break; + } case 91: #line 701 "test_spec_parse.y" - { - (yyval.cmd) = (yyvsp[(1) - (1)].cmd); - break; - } + { + (yyval.cmd) = (yyvsp[(1) - (1)].cmd); + break; + } case 92: #line 702 "test_spec_parse.y" - { - (yyval.cmd) = (yyvsp[(1) - (1)].cmd); - break; - } + { + (yyval.cmd) = (yyvsp[(1) - (1)].cmd); + break; + } case 93: #line 703 "test_spec_parse.y" - { - (yyval.cmd) = (yyvsp[(1) - (1)].cmd); - break; - } + { + (yyval.cmd) = (yyvsp[(1) - (1)].cmd); + break; + } case 94: #line 704 "test_spec_parse.y" - { - (yyval.cmd) = (yyvsp[(1) - (1)].cmd); - break; - } + { + (yyval.cmd) = (yyvsp[(1) - (1)].cmd); + break; + } case 95: #line 705 "test_spec_parse.y" - { - (yyval.cmd) = (yyvsp[(1) - (1)].cmd); - break; - } + { + (yyval.cmd) = (yyvsp[(1) - (1)].cmd); + break; + } case 96: #line 706 "test_spec_parse.y" - { - (yyval.cmd) = (yyvsp[(1) - (1)].cmd); - break; - } + { + (yyval.cmd) = (yyvsp[(1) - (1)].cmd); + break; + } case 97: #line 707 "test_spec_parse.y" - { - (yyval.cmd) = (yyvsp[(1) - (1)].cmd); - break; - } + { + (yyval.cmd) = (yyvsp[(1) - (1)].cmd); + break; + } case 98: #line 708 "test_spec_parse.y" - { - (yyval.cmd) = (yyvsp[(1) - (1)].cmd); - break; - } + { + (yyval.cmd) = (yyvsp[(1) - (1)].cmd); + break; + } case 99: #line 723 "test_spec_parse.y" - { - (yyval.cmd) = make_cmd(CMD_EXEC); - strlcpy((yyval.cmd)->service, (yyvsp[(2) - (3)].str), - sizeof((yyval.cmd)->service)); - strlcpy((yyval.cmd)->args, (yyvsp[(3) - (3)].str), - sizeof((yyval.cmd)->args)); - free((yyvsp[(2) - (3)].str)); - free((yyvsp[(3) - (3)].str)); - break; - } + { + (yyval.cmd) = make_cmd(CMD_EXEC); + strlcpy((yyval.cmd)->service, (yyvsp[(2) - (3)].str), + sizeof((yyval.cmd)->service)); + strlcpy((yyval.cmd)->args, (yyvsp[(3) - (3)].str), + sizeof((yyval.cmd)->args)); + free((yyvsp[(2) - (3)].str)); + free((yyvsp[(3) - (3)].str)); + break; + } case 100: #line 730 "test_spec_parse.y" - { - (yyval.cmd) = make_cmd(CMD_EXEC); - strlcpy((yyval.cmd)->service, (yyvsp[(2) - (2)].str), - sizeof((yyval.cmd)->service)); - free((yyvsp[(2) - (2)].str)); - break; - } + { + (yyval.cmd) = make_cmd(CMD_EXEC); + strlcpy((yyval.cmd)->service, (yyvsp[(2) - (2)].str), + sizeof((yyval.cmd)->service)); + free((yyvsp[(2) - (2)].str)); + break; + } case 101: #line 736 "test_spec_parse.y" - { - (yyval.cmd) = make_cmd(CMD_EXEC_FAILS); - strlcpy((yyval.cmd)->service, (yyvsp[(2) - (3)].str), - sizeof((yyval.cmd)->service)); - strlcpy((yyval.cmd)->args, (yyvsp[(3) - (3)].str), - sizeof((yyval.cmd)->args)); - free((yyvsp[(2) - (3)].str)); - free((yyvsp[(3) - (3)].str)); - break; - } + { + (yyval.cmd) = make_cmd(CMD_EXEC_FAILS); + strlcpy((yyval.cmd)->service, (yyvsp[(2) - (3)].str), + sizeof((yyval.cmd)->service)); + strlcpy((yyval.cmd)->args, (yyvsp[(3) - (3)].str), + sizeof((yyval.cmd)->args)); + free((yyvsp[(2) - (3)].str)); + free((yyvsp[(3) - (3)].str)); + break; + } case 102: #line 743 "test_spec_parse.y" - { - (yyval.cmd) = make_cmd(CMD_EXEC_FAILS); - strlcpy((yyval.cmd)->service, (yyvsp[(2) - (2)].str), - sizeof((yyval.cmd)->service)); - free((yyvsp[(2) - (2)].str)); - break; - } + { + (yyval.cmd) = make_cmd(CMD_EXEC_FAILS); + strlcpy((yyval.cmd)->service, (yyvsp[(2) - (2)].str), + sizeof((yyval.cmd)->service)); + free((yyvsp[(2) - (2)].str)); + break; + } case 103: #line 749 "test_spec_parse.y" - { - /* "pg_autoctl perform failover --formation auth" - * EXEC_ARGS returns T_IDENT for first word, T_SHELL_ARGS for rest */ - (yyval.cmd) = make_cmd(CMD_PG_AUTOCTL); - sformat((yyval.cmd)->args, sizeof((yyval.cmd)->args), "%s %s", - (yyvsp[(2) - (3)].str), (yyvsp[(3) - (3)].str)); - free((yyvsp[(2) - (3)].str)); - free((yyvsp[(3) - (3)].str)); - break; - } + { + /* "pg_autoctl perform failover --formation auth" + * EXEC_ARGS returns T_IDENT for first word, T_SHELL_ARGS for rest */ + (yyval.cmd) = make_cmd(CMD_PG_AUTOCTL); + sformat((yyval.cmd)->args, sizeof((yyval.cmd)->args), "%s %s", + (yyvsp[(2) - (3)].str), (yyvsp[(3) - (3)].str)); + free((yyvsp[(2) - (3)].str)); + free((yyvsp[(3) - (3)].str)); + break; + } case 104: #line 757 "test_spec_parse.y" - { - (yyval.cmd) = make_cmd(CMD_PG_AUTOCTL); - strlcpy((yyval.cmd)->args, (yyvsp[(2) - (2)].str), - sizeof((yyval.cmd)->args)); - free((yyvsp[(2) - (2)].str)); - break; - } + { + (yyval.cmd) = make_cmd(CMD_PG_AUTOCTL); + strlcpy((yyval.cmd)->args, (yyvsp[(2) - (2)].str), + sizeof((yyval.cmd)->args)); + free((yyvsp[(2) - (2)].str)); + break; + } case 105: #line 763 "test_spec_parse.y" - { - (yyval.cmd) = make_cmd(CMD_PG_AUTOCTL); - break; - } + { + (yyval.cmd) = make_cmd(CMD_PG_AUTOCTL); + break; + } case 108: #line 801 "test_spec_parse.y" - { - if (!current_wait_cmd) - { - current_wait_cmd = make_cmd(CMD_WAIT_MULTI); - } - int i = current_wait_cmd->waitStateCount; - if (i < PGAF_MAX_WAIT_STATES) { - strlcpy(current_wait_cmd->waitNodes[i], (yyvsp[(1) - (4)].str), - sizeof(current_wait_cmd->waitNodes[0])); - strlcpy(current_wait_cmd->waitStates[i], (yyvsp[(4) - (4)].str), - sizeof(current_wait_cmd->waitStates[0])); - current_wait_cmd->waitStateCount++; + if (!current_wait_cmd) + { + current_wait_cmd = make_cmd(CMD_WAIT_MULTI); + } + int i = current_wait_cmd->waitStateCount; + if (i < PGAF_MAX_WAIT_STATES) + { + strlcpy(current_wait_cmd->waitNodes[i], (yyvsp[(1) - (4)].str), + sizeof(current_wait_cmd->waitNodes[0])); + strlcpy(current_wait_cmd->waitStates[i], (yyvsp[(4) - (4)].str), + sizeof(current_wait_cmd->waitStates[0])); + current_wait_cmd->waitStateCount++; + } + free((yyvsp[(1) - (4)].str)); + break; } - free((yyvsp[(1) - (4)].str)); - break; - } case 109: #line 816 "test_spec_parse.y" - { - if (!current_wait_cmd) { - current_wait_cmd = make_cmd(CMD_WAIT_MULTI); - } - int i = current_wait_cmd->waitStateCount; - if (i < PGAF_MAX_WAIT_STATES) - { - strlcpy(current_wait_cmd->waitNodes[i], (yyvsp[(1) - (4)].str), - sizeof(current_wait_cmd->waitNodes[0])); - strlcpy(current_wait_cmd->waitStates[i], (yyvsp[(4) - (4)].str), - sizeof(current_wait_cmd->waitStates[0])); - current_wait_cmd->waitStateCount++; + if (!current_wait_cmd) + { + current_wait_cmd = make_cmd(CMD_WAIT_MULTI); + } + int i = current_wait_cmd->waitStateCount; + if (i < PGAF_MAX_WAIT_STATES) + { + strlcpy(current_wait_cmd->waitNodes[i], (yyvsp[(1) - (4)].str), + sizeof(current_wait_cmd->waitNodes[0])); + strlcpy(current_wait_cmd->waitStates[i], (yyvsp[(4) - (4)].str), + sizeof(current_wait_cmd->waitStates[0])); + current_wait_cmd->waitStateCount++; + } + free((yyvsp[(1) - (4)].str)); + free((yyvsp[(4) - (4)].str)); + break; } - free((yyvsp[(1) - (4)].str)); - free((yyvsp[(4) - (4)].str)); - break; - } case 114: #line 856 "test_spec_parse.y" - { - /* current_pass_cmd set by the enclosing wait_cmd rule */ - if (current_pass_cmd && - current_pass_cmd->passThroughCount < PGAF_MAX_WAIT_STATES) { - strlcpy( - current_pass_cmd->passThroughStates[current_pass_cmd-> - passThroughCount++], - (yyvsp[(1) - (1)].str), - sizeof(current_pass_cmd->passThroughStates[0])); + /* current_pass_cmd set by the enclosing wait_cmd rule */ + if (current_pass_cmd && + current_pass_cmd->passThroughCount < PGAF_MAX_WAIT_STATES) + { + strlcpy( + current_pass_cmd->passThroughStates[current_pass_cmd-> + passThroughCount++], + (yyvsp[(1) - (1)].str), + sizeof(current_pass_cmd->passThroughStates[0])); + } + break; } - break; - } case 115: #line 864 "test_spec_parse.y" - { - if (current_pass_cmd && - current_pass_cmd->passThroughCount < PGAF_MAX_WAIT_STATES) { - strlcpy( - current_pass_cmd->passThroughStates[current_pass_cmd-> - passThroughCount++], - (yyvsp[(1) - (1)].str), - sizeof(current_pass_cmd->passThroughStates[0])); + if (current_pass_cmd && + current_pass_cmd->passThroughCount < PGAF_MAX_WAIT_STATES) + { + strlcpy( + current_pass_cmd->passThroughStates[current_pass_cmd-> + passThroughCount++], + (yyvsp[(1) - (1)].str), + sizeof(current_pass_cmd->passThroughStates[0])); + } + free((yyvsp[(1) - (1)].str)); + break; } - free((yyvsp[(1) - (1)].str)); - break; - } case 116: #line 872 "test_spec_parse.y" - { - if (current_pass_cmd && - current_pass_cmd->passThroughCount < PGAF_MAX_WAIT_STATES) { - strlcpy( - current_pass_cmd->passThroughStates[current_pass_cmd-> - passThroughCount++], - (yyvsp[(3) - (3)].str), - sizeof(current_pass_cmd->passThroughStates[0])); + if (current_pass_cmd && + current_pass_cmd->passThroughCount < PGAF_MAX_WAIT_STATES) + { + strlcpy( + current_pass_cmd->passThroughStates[current_pass_cmd-> + passThroughCount++], + (yyvsp[(3) - (3)].str), + sizeof(current_pass_cmd->passThroughStates[0])); + } + break; } - break; - } case 117: #line 879 "test_spec_parse.y" - { - if (current_pass_cmd && - current_pass_cmd->passThroughCount < PGAF_MAX_WAIT_STATES) { - strlcpy( - current_pass_cmd->passThroughStates[current_pass_cmd-> - passThroughCount++], - (yyvsp[(3) - (3)].str), - sizeof(current_pass_cmd->passThroughStates[0])); + if (current_pass_cmd && + current_pass_cmd->passThroughCount < PGAF_MAX_WAIT_STATES) + { + strlcpy( + current_pass_cmd->passThroughStates[current_pass_cmd-> + passThroughCount++], + (yyvsp[(3) - (3)].str), + sizeof(current_pass_cmd->passThroughStates[0])); + } + free((yyvsp[(3) - (3)].str)); + break; } - free((yyvsp[(3) - (3)].str)); - break; - } case 118: #line 890 "test_spec_parse.y" - { - current_pass_cmd = make_cmd(CMD_WAIT_STATE); - strlcpy(current_pass_cmd->service, (yyvsp[(3) - (6)].str), - sizeof(current_pass_cmd->service)); - strlcpy(current_pass_cmd->state, (yyvsp[(6) - (6)].str), - sizeof(current_pass_cmd->state)); - free((yyvsp[(3) - (6)].str)); - break; - } + { + current_pass_cmd = make_cmd(CMD_WAIT_STATE); + strlcpy(current_pass_cmd->service, (yyvsp[(3) - (6)].str), + sizeof(current_pass_cmd->service)); + strlcpy(current_pass_cmd->state, (yyvsp[(6) - (6)].str), + sizeof(current_pass_cmd->state)); + free((yyvsp[(3) - (6)].str)); + break; + } case 119: #line 895 "test_spec_parse.y" - { - current_pass_cmd->timeoutSeconds = (yyvsp[(9) - (9)].ival); - (yyval.cmd) = current_pass_cmd; - current_pass_cmd = NULL; - break; - } + { + current_pass_cmd->timeoutSeconds = (yyvsp[(9) - (9)].ival); + (yyval.cmd) = current_pass_cmd; + current_pass_cmd = NULL; + break; + } case 120: #line 901 "test_spec_parse.y" - { - current_pass_cmd = make_cmd(CMD_WAIT_STATE); - strlcpy(current_pass_cmd->service, (yyvsp[(3) - (6)].str), - sizeof(current_pass_cmd->service)); - strlcpy(current_pass_cmd->state, (yyvsp[(6) - (6)].str), - sizeof(current_pass_cmd->state)); - free((yyvsp[(3) - (6)].str)); - free((yyvsp[(6) - (6)].str)); - break; - } + { + current_pass_cmd = make_cmd(CMD_WAIT_STATE); + strlcpy(current_pass_cmd->service, (yyvsp[(3) - (6)].str), + sizeof(current_pass_cmd->service)); + strlcpy(current_pass_cmd->state, (yyvsp[(6) - (6)].str), + sizeof(current_pass_cmd->state)); + free((yyvsp[(3) - (6)].str)); + free((yyvsp[(6) - (6)].str)); + break; + } case 121: #line 906 "test_spec_parse.y" - { - current_pass_cmd->timeoutSeconds = (yyvsp[(9) - (9)].ival); - (yyval.cmd) = current_pass_cmd; - current_pass_cmd = NULL; - break; - } + { + current_pass_cmd->timeoutSeconds = (yyvsp[(9) - (9)].ival); + (yyval.cmd) = current_pass_cmd; + current_pass_cmd = NULL; + break; + } case 122: #line 912 "test_spec_parse.y" - { - (yyval.cmd) = make_cmd(CMD_WAIT_STATE); - (yyval.cmd)->kind = CMD_ASSERT_ASSIGNED; - strlcpy((yyval.cmd)->service, (yyvsp[(3) - (7)].str), - sizeof((yyval.cmd)->service)); - strlcpy((yyval.cmd)->state, (yyvsp[(6) - (7)].str), - sizeof((yyval.cmd)->state)); - (yyval.cmd)->timeoutSeconds = (yyvsp[(7) - (7)].ival); - free((yyvsp[(3) - (7)].str)); - break; - } + { + (yyval.cmd) = make_cmd(CMD_WAIT_STATE); + (yyval.cmd)->kind = CMD_ASSERT_ASSIGNED; + strlcpy((yyval.cmd)->service, (yyvsp[(3) - (7)].str), + sizeof((yyval.cmd)->service)); + strlcpy((yyval.cmd)->state, (yyvsp[(6) - (7)].str), + sizeof((yyval.cmd)->state)); + (yyval.cmd)->timeoutSeconds = (yyvsp[(7) - (7)].ival); + free((yyvsp[(3) - (7)].str)); + break; + } case 123: #line 921 "test_spec_parse.y" - { - (yyval.cmd) = make_cmd(CMD_WAIT_STATE); - (yyval.cmd)->kind = CMD_ASSERT_ASSIGNED; - strlcpy((yyval.cmd)->service, (yyvsp[(3) - (7)].str), - sizeof((yyval.cmd)->service)); - strlcpy((yyval.cmd)->state, (yyvsp[(6) - (7)].str), - sizeof((yyval.cmd)->state)); - (yyval.cmd)->timeoutSeconds = (yyvsp[(7) - (7)].ival); - free((yyvsp[(3) - (7)].str)); - free((yyvsp[(6) - (7)].str)); - break; - } + { + (yyval.cmd) = make_cmd(CMD_WAIT_STATE); + (yyval.cmd)->kind = CMD_ASSERT_ASSIGNED; + strlcpy((yyval.cmd)->service, (yyvsp[(3) - (7)].str), + sizeof((yyval.cmd)->service)); + strlcpy((yyval.cmd)->state, (yyvsp[(6) - (7)].str), + sizeof((yyval.cmd)->state)); + (yyval.cmd)->timeoutSeconds = (yyvsp[(7) - (7)].ival); + free((yyvsp[(3) - (7)].str)); + free((yyvsp[(6) - (7)].str)); + break; + } case 124: #line 930 "test_spec_parse.y" - { - (yyval.cmd) = make_cmd(CMD_WAIT_STOPPED); - strlcpy((yyval.cmd)->service, (yyvsp[(3) - (5)].str), - sizeof((yyval.cmd)->service)); - (yyval.cmd)->timeoutSeconds = (yyvsp[(5) - (5)].ival); - free((yyvsp[(3) - (5)].str)); - break; - } + { + (yyval.cmd) = make_cmd(CMD_WAIT_STOPPED); + strlcpy((yyval.cmd)->service, (yyvsp[(3) - (5)].str), + sizeof((yyval.cmd)->service)); + (yyval.cmd)->timeoutSeconds = (yyvsp[(5) - (5)].ival); + free((yyvsp[(3) - (5)].str)); + break; + } case 125: #line 937 "test_spec_parse.y" - { - (yyval.cmd) = current_wait_cmd; - (yyval.cmd)->timeoutSeconds = (yyvsp[(5) - (5)].ival); - current_wait_cmd = NULL; - break; - } + { + (yyval.cmd) = current_wait_cmd; + (yyval.cmd)->timeoutSeconds = (yyvsp[(5) - (5)].ival); + current_wait_cmd = NULL; + break; + } case 126: #line 951 "test_spec_parse.y" - { - (yyval.cmd) = current_wait_cmd; - (yyval.cmd)->timeoutSeconds = (yyvsp[(6) - (6)].ival); - current_wait_cmd = NULL; - break; - } + { + (yyval.cmd) = current_wait_cmd; + (yyval.cmd)->timeoutSeconds = (yyvsp[(6) - (6)].ival); + current_wait_cmd = NULL; + break; + } case 127: #line 966 "test_spec_parse.y" - { - current_wait_cmd = make_cmd(CMD_WAIT_STATES); - strlcpy(current_wait_cmd->waitStates[current_wait_cmd->waitStateCount++], - (yyvsp[(1) - (1)].str), sizeof(current_wait_cmd->waitStates[0])); - break; - } + { + current_wait_cmd = make_cmd(CMD_WAIT_STATES); + strlcpy(current_wait_cmd->waitStates[current_wait_cmd->waitStateCount++], + (yyvsp[(1) - (1)].str), sizeof(current_wait_cmd->waitStates[0])); + break; + } case 128: #line 972 "test_spec_parse.y" - { - current_wait_cmd = make_cmd(CMD_WAIT_STATES); - strlcpy(current_wait_cmd->waitStates[current_wait_cmd->waitStateCount++], - (yyvsp[(1) - (1)].str), sizeof(current_wait_cmd->waitStates[0])); - free((yyvsp[(1) - (1)].str)); - break; - } + { + current_wait_cmd = make_cmd(CMD_WAIT_STATES); + strlcpy(current_wait_cmd->waitStates[current_wait_cmd->waitStateCount++], + (yyvsp[(1) - (1)].str), sizeof(current_wait_cmd->waitStates[0])); + free((yyvsp[(1) - (1)].str)); + break; + } case 129: #line 979 "test_spec_parse.y" - { - if (current_wait_cmd->waitStateCount < PGAF_MAX_WAIT_STATES) { - strlcpy( - current_wait_cmd->waitStates[current_wait_cmd->waitStateCount++], - (yyvsp[(3) - (3)].str), - sizeof(current_wait_cmd->waitStates[0])); + if (current_wait_cmd->waitStateCount < PGAF_MAX_WAIT_STATES) + { + strlcpy( + current_wait_cmd->waitStates[current_wait_cmd->waitStateCount++], + (yyvsp[(3) - (3)].str), + sizeof(current_wait_cmd->waitStates[0])); + } + break; } - break; - } case 130: #line 985 "test_spec_parse.y" - { - if (current_wait_cmd->waitStateCount < PGAF_MAX_WAIT_STATES) { - strlcpy( - current_wait_cmd->waitStates[current_wait_cmd->waitStateCount++], - (yyvsp[(3) - (3)].str), - sizeof(current_wait_cmd->waitStates[0])); + if (current_wait_cmd->waitStateCount < PGAF_MAX_WAIT_STATES) + { + strlcpy( + current_wait_cmd->waitStates[current_wait_cmd->waitStateCount++], + (yyvsp[(3) - (3)].str), + sizeof(current_wait_cmd->waitStates[0])); + } + free((yyvsp[(3) - (3)].str)); + break; } - free((yyvsp[(3) - (3)].str)); - break; - } case 133: #line 1004 "test_spec_parse.y" - { - if (current_wait_cmd->waitGroupCount < PGAF_MAX_WAIT_GROUPS) { - current_wait_cmd->waitGroups[current_wait_cmd->waitGroupCount++] = - (yyvsp[(2) - (2)].ival); + if (current_wait_cmd->waitGroupCount < PGAF_MAX_WAIT_GROUPS) + { + current_wait_cmd->waitGroups[current_wait_cmd->waitGroupCount++] = + (yyvsp[(2) - (2)].ival); + } + break; } - break; - } case 134: #line 1009 "test_spec_parse.y" - { - if (current_wait_cmd->waitGroupCount < PGAF_MAX_WAIT_GROUPS) { - current_wait_cmd->waitGroups[current_wait_cmd->waitGroupCount++] = - (yyvsp[(4) - (4)].ival); + if (current_wait_cmd->waitGroupCount < PGAF_MAX_WAIT_GROUPS) + { + current_wait_cmd->waitGroups[current_wait_cmd->waitGroupCount++] = + (yyvsp[(4) - (4)].ival); + } + break; } - break; - } case 135: #line 1016 "test_spec_parse.y" - { - (yyval.ival) = PGAF_TIMEOUT_DEFAULT; - break; - } + { + (yyval.ival) = PGAF_TIMEOUT_DEFAULT; + break; + } case 136: #line 1017 "test_spec_parse.y" - { - (yyval.ival) = (yyvsp[(2) - (2)].ival); - break; - } + { + (yyval.ival) = (yyvsp[(2) - (2)].ival); + break; + } case 137: #line 1018 "test_spec_parse.y" - { - (yyval.ival) = (yyvsp[(3) - (3)].ival); - break; - } + { + (yyval.ival) = (yyvsp[(3) - (3)].ival); + break; + } case 138: #line 1030 "test_spec_parse.y" - { - (yyval.cmd) = make_cmd((yyvsp[(6) - (6)].ival) > 0 ? CMD_WAIT_STATE : - CMD_ASSERT_STATE); - strlcpy((yyval.cmd)->service, (yyvsp[(2) - (6)].str), - sizeof((yyval.cmd)->service)); - strlcpy((yyval.cmd)->state, (yyvsp[(5) - (6)].str), - sizeof((yyval.cmd)->state)); - (yyval.cmd)->timeoutSeconds = (yyvsp[(6) - (6)].ival); - free((yyvsp[(2) - (6)].str)); - break; - } + { + (yyval.cmd) = make_cmd((yyvsp[(6) - (6)].ival) > 0 ? CMD_WAIT_STATE : + CMD_ASSERT_STATE); + strlcpy((yyval.cmd)->service, (yyvsp[(2) - (6)].str), + sizeof((yyval.cmd)->service)); + strlcpy((yyval.cmd)->state, (yyvsp[(5) - (6)].str), + sizeof((yyval.cmd)->state)); + (yyval.cmd)->timeoutSeconds = (yyvsp[(6) - (6)].ival); + free((yyvsp[(2) - (6)].str)); + break; + } case 139: #line 1038 "test_spec_parse.y" - { - (yyval.cmd) = make_cmd((yyvsp[(6) - (6)].ival) > 0 ? CMD_WAIT_STATE : - CMD_ASSERT_STATE); - strlcpy((yyval.cmd)->service, (yyvsp[(2) - (6)].str), - sizeof((yyval.cmd)->service)); - strlcpy((yyval.cmd)->state, (yyvsp[(5) - (6)].str), - sizeof((yyval.cmd)->state)); - (yyval.cmd)->timeoutSeconds = (yyvsp[(6) - (6)].ival); - free((yyvsp[(2) - (6)].str)); - free((yyvsp[(5) - (6)].str)); - break; - } + { + (yyval.cmd) = make_cmd((yyvsp[(6) - (6)].ival) > 0 ? CMD_WAIT_STATE : + CMD_ASSERT_STATE); + strlcpy((yyval.cmd)->service, (yyvsp[(2) - (6)].str), + sizeof((yyval.cmd)->service)); + strlcpy((yyval.cmd)->state, (yyvsp[(5) - (6)].str), + sizeof((yyval.cmd)->state)); + (yyval.cmd)->timeoutSeconds = (yyvsp[(6) - (6)].ival); + free((yyvsp[(2) - (6)].str)); + free((yyvsp[(5) - (6)].str)); + break; + } case 140: #line 1046 "test_spec_parse.y" - { - (yyval.cmd) = make_cmd(CMD_ASSERT_ASSIGNED); - strlcpy((yyval.cmd)->service, (yyvsp[(2) - (6)].str), - sizeof((yyval.cmd)->service)); - strlcpy((yyval.cmd)->state, (yyvsp[(5) - (6)].str), - sizeof((yyval.cmd)->state)); - (yyval.cmd)->timeoutSeconds = (yyvsp[(6) - (6)].ival); - free((yyvsp[(2) - (6)].str)); - break; - } + { + (yyval.cmd) = make_cmd(CMD_ASSERT_ASSIGNED); + strlcpy((yyval.cmd)->service, (yyvsp[(2) - (6)].str), + sizeof((yyval.cmd)->service)); + strlcpy((yyval.cmd)->state, (yyvsp[(5) - (6)].str), + sizeof((yyval.cmd)->state)); + (yyval.cmd)->timeoutSeconds = (yyvsp[(6) - (6)].ival); + free((yyvsp[(2) - (6)].str)); + break; + } case 141: #line 1054 "test_spec_parse.y" - { - (yyval.cmd) = make_cmd(CMD_ASSERT_ASSIGNED); - strlcpy((yyval.cmd)->service, (yyvsp[(2) - (6)].str), - sizeof((yyval.cmd)->service)); - strlcpy((yyval.cmd)->state, (yyvsp[(5) - (6)].str), - sizeof((yyval.cmd)->state)); - (yyval.cmd)->timeoutSeconds = (yyvsp[(6) - (6)].ival); - free((yyvsp[(2) - (6)].str)); - free((yyvsp[(5) - (6)].str)); - break; - } + { + (yyval.cmd) = make_cmd(CMD_ASSERT_ASSIGNED); + strlcpy((yyval.cmd)->service, (yyvsp[(2) - (6)].str), + sizeof((yyval.cmd)->service)); + strlcpy((yyval.cmd)->state, (yyvsp[(5) - (6)].str), + sizeof((yyval.cmd)->state)); + (yyval.cmd)->timeoutSeconds = (yyvsp[(6) - (6)].ival); + free((yyvsp[(2) - (6)].str)); + free((yyvsp[(5) - (6)].str)); + break; + } case 142: #line 1072 "test_spec_parse.y" - { - (yyval.cmd) = make_cmd(CMD_SQL); - strlcpy((yyval.cmd)->service, (yyvsp[(2) - (3)].str), - sizeof((yyval.cmd)->service)); - strlcpy((yyval.cmd)->args, (yyvsp[(3) - (3)].str), - sizeof((yyval.cmd)->args)); - free((yyvsp[(2) - (3)].str)); - free((yyvsp[(3) - (3)].str)); - break; - } + { + (yyval.cmd) = make_cmd(CMD_SQL); + strlcpy((yyval.cmd)->service, (yyvsp[(2) - (3)].str), + sizeof((yyval.cmd)->service)); + strlcpy((yyval.cmd)->args, (yyvsp[(3) - (3)].str), + sizeof((yyval.cmd)->args)); + free((yyvsp[(2) - (3)].str)); + free((yyvsp[(3) - (3)].str)); + break; + } case 143: #line 1087 "test_spec_parse.y" - { - (yyval.cmd) = make_cmd(CMD_EXPECT); - strlcpy((yyval.cmd)->expected, (yyvsp[(2) - (2)].str), - sizeof((yyval.cmd)->expected)); - expand_tuple_expect((yyval.cmd)->expected, sizeof((yyval.cmd)->expected)); - free((yyvsp[(2) - (2)].str)); - break; - } + { + (yyval.cmd) = make_cmd(CMD_EXPECT); + strlcpy((yyval.cmd)->expected, (yyvsp[(2) - (2)].str), + sizeof((yyval.cmd)->expected)); + expand_tuple_expect((yyval.cmd)->expected, sizeof((yyval.cmd)->expected)); + free((yyvsp[(2) - (2)].str)); + break; + } case 144: #line 1094 "test_spec_parse.y" - { - (yyval.cmd) = make_cmd(CMD_EXPECT_ERROR); - break; - } + { + (yyval.cmd) = make_cmd(CMD_EXPECT_ERROR); + break; + } case 145: #line 1098 "test_spec_parse.y" - { - (yyval.cmd) = make_cmd(CMD_EXPECT_ERROR); - strlcpy((yyval.cmd)->state, (yyvsp[(3) - (3)].str), - sizeof((yyval.cmd)->state)); - free((yyvsp[(3) - (3)].str)); - break; - } + { + (yyval.cmd) = make_cmd(CMD_EXPECT_ERROR); + strlcpy((yyval.cmd)->state, (yyvsp[(3) - (3)].str), + sizeof((yyval.cmd)->state)); + free((yyvsp[(3) - (3)].str)); + break; + } case 146: #line 1104 "test_spec_parse.y" - { - /* SQLSTATE codes like 25006 are all digits, lexed as T_INTEGER */ - (yyval.cmd) = make_cmd(CMD_EXPECT_ERROR); - snprintf(/* IGNORE-BANNED */ (yyval.cmd)->state, - sizeof((yyval.cmd)->state), "%d", - (yyvsp[(3) - (3)].ival)); - break; - } + { + /* SQLSTATE codes like 25006 are all digits, lexed as T_INTEGER */ + (yyval.cmd) = make_cmd(CMD_EXPECT_ERROR); + snprintf(/* IGNORE-BANNED */ (yyval.cmd)->state, + sizeof((yyval.cmd)->state), "%d", + (yyvsp[(3) - (3)].ival)); + break; + } case 147: #line 1117 "test_spec_parse.y" - { - (yyval.cmd) = current_promote_cmd; - current_promote_cmd = NULL; - break; - } + { + (yyval.cmd) = current_promote_cmd; + current_promote_cmd = NULL; + break; + } case 148: #line 1125 "test_spec_parse.y" - { - current_promote_cmd = make_cmd(CMD_PROMOTE); - current_promote_cmd->timeoutSeconds = PGAF_TIMEOUT_DEFAULT; - strlcpy( - current_promote_cmd->promoteNodes[current_promote_cmd->promoteCount++], - (yyvsp[(1) - (1)].str), - sizeof(current_promote_cmd->promoteNodes[0])); - free((yyvsp[(1) - (1)].str)); - break; - } + { + current_promote_cmd = make_cmd(CMD_PROMOTE); + current_promote_cmd->timeoutSeconds = PGAF_TIMEOUT_DEFAULT; + strlcpy( + current_promote_cmd->promoteNodes[current_promote_cmd->promoteCount++], + (yyvsp[(1) - (1)].str), + sizeof(current_promote_cmd->promoteNodes[0])); + free((yyvsp[(1) - (1)].str)); + break; + } case 149: #line 1133 "test_spec_parse.y" - { - if (current_promote_cmd->promoteCount < PGAF_MAX_PROMOTE_NODES) { - strlcpy( - current_promote_cmd->promoteNodes[current_promote_cmd-> - promoteCount++], - (yyvsp[(3) - (3)].str), - sizeof(current_promote_cmd->promoteNodes[0])); + if (current_promote_cmd->promoteCount < PGAF_MAX_PROMOTE_NODES) + { + strlcpy( + current_promote_cmd->promoteNodes[current_promote_cmd-> + promoteCount++], + (yyvsp[(3) - (3)].str), + sizeof(current_promote_cmd->promoteNodes[0])); + } + free((yyvsp[(3) - (3)].str)); + break; } - free((yyvsp[(3) - (3)].str)); - break; - } case 150: #line 1148 "test_spec_parse.y" - { - (yyval.cmd) = make_cmd(CMD_NETWORK_OFF); - strlcpy((yyval.cmd)->service, (yyvsp[(3) - (3)].str), - sizeof((yyval.cmd)->service)); - free((yyvsp[(3) - (3)].str)); - break; - } + { + (yyval.cmd) = make_cmd(CMD_NETWORK_OFF); + strlcpy((yyval.cmd)->service, (yyvsp[(3) - (3)].str), + sizeof((yyval.cmd)->service)); + free((yyvsp[(3) - (3)].str)); + break; + } case 151: #line 1154 "test_spec_parse.y" - { - (yyval.cmd) = make_cmd(CMD_NETWORK_ON); - strlcpy((yyval.cmd)->service, (yyvsp[(3) - (3)].str), - sizeof((yyval.cmd)->service)); - free((yyvsp[(3) - (3)].str)); - break; - } + { + (yyval.cmd) = make_cmd(CMD_NETWORK_ON); + strlcpy((yyval.cmd)->service, (yyvsp[(3) - (3)].str), + sizeof((yyval.cmd)->service)); + free((yyvsp[(3) - (3)].str)); + break; + } case 152: #line 1167 "test_spec_parse.y" - { - (yyval.cmd) = make_cmd(CMD_SLEEP); - (yyval.cmd)->timeoutSeconds = (yyvsp[(2) - (2)].ival); - break; - } + { + (yyval.cmd) = make_cmd(CMD_SLEEP); + (yyval.cmd)->timeoutSeconds = (yyvsp[(2) - (2)].ival); + break; + } case 153: #line 1181 "test_spec_parse.y" - { - (yyval.cmd) = make_cmd(CMD_COMPOSE_DOWN); - break; - } + { + (yyval.cmd) = make_cmd(CMD_COMPOSE_DOWN); + break; + } case 154: #line 1185 "test_spec_parse.y" - { - (yyval.cmd) = make_cmd(CMD_COMPOSE_START); - strlcpy((yyval.cmd)->service, (yyvsp[(3) - (3)].str), - sizeof((yyval.cmd)->service)); - free((yyvsp[(3) - (3)].str)); - break; - } + { + (yyval.cmd) = make_cmd(CMD_COMPOSE_START); + strlcpy((yyval.cmd)->service, (yyvsp[(3) - (3)].str), + sizeof((yyval.cmd)->service)); + free((yyvsp[(3) - (3)].str)); + break; + } case 155: #line 1191 "test_spec_parse.y" - { - (yyval.cmd) = make_cmd(CMD_COMPOSE_STOP); - strlcpy((yyval.cmd)->service, (yyvsp[(3) - (3)].str), - sizeof((yyval.cmd)->service)); - free((yyvsp[(3) - (3)].str)); - break; - } + { + (yyval.cmd) = make_cmd(CMD_COMPOSE_STOP); + strlcpy((yyval.cmd)->service, (yyvsp[(3) - (3)].str), + sizeof((yyval.cmd)->service)); + free((yyvsp[(3) - (3)].str)); + break; + } case 156: #line 1197 "test_spec_parse.y" - { - (yyval.cmd) = make_cmd(CMD_COMPOSE_KILL); - strlcpy((yyval.cmd)->service, (yyvsp[(3) - (3)].str), - sizeof((yyval.cmd)->service)); - free((yyvsp[(3) - (3)].str)); - break; - } + { + (yyval.cmd) = make_cmd(CMD_COMPOSE_KILL); + strlcpy((yyval.cmd)->service, (yyvsp[(3) - (3)].str), + sizeof((yyval.cmd)->service)); + free((yyvsp[(3) - (3)].str)); + break; + } case 157: #line 1223 "test_spec_parse.y" - { - (yyval.cmd) = make_cmd(CMD_COMPOSE_INJECT); - strlcpy((yyval.cmd)->expected, (yyvsp[(3) - (4)].str), - sizeof((yyval.cmd)->expected)); /* image */ - - /* Split T_SHELL_ARGS: " :" */ - char tmp[4096]; - strlcpy(tmp, (yyvsp[(4) - (4)].str), sizeof(tmp)); - char *src = tmp; - char *p = tmp; - while (*p && *p != ' ' && *p != '\t') - { - p++; - } - if (*p) { - *p++ = '\0'; - while (*p == ' ' || *p == '\t') + (yyval.cmd) = make_cmd(CMD_COMPOSE_INJECT); + strlcpy((yyval.cmd)->expected, (yyvsp[(3) - (4)].str), + sizeof((yyval.cmd)->expected)); /* image */ + + /* Split T_SHELL_ARGS: " :" */ + char tmp[4096]; + strlcpy(tmp, (yyvsp[(4) - (4)].str), sizeof(tmp)); + char *src = tmp; + char *p = tmp; + while (*p && *p != ' ' && *p != '\t') { p++; } + if (*p) + { + *p++ = '\0'; + while (*p == ' ' || *p == '\t') + { + p++; + } + } + char *svcdst = p; + char *colon = (*svcdst) ? strchr(svcdst, ':') : NULL; + strlcpy((yyval.cmd)->args, src, sizeof((yyval.cmd)->args)); + if (colon) + { + *colon = '\0'; + strlcpy((yyval.cmd)->service, svcdst, sizeof((yyval.cmd)->service)); /* dst svc */ + strlcpy((yyval.cmd)->state, colon + 1, sizeof((yyval.cmd)->state)); /* dst path */ + } + free((yyvsp[(3) - (4)].str)); + free((yyvsp[(4) - (4)].str)); + break; } - char *svcdst = p; - char *colon = (*svcdst) ? strchr(svcdst, ':') : NULL; - strlcpy((yyval.cmd)->args, src, sizeof((yyval.cmd)->args)); - if (colon) - { - *colon = '\0'; - strlcpy((yyval.cmd)->service, svcdst, sizeof((yyval.cmd)->service)); /* dst svc */ - strlcpy((yyval.cmd)->state, colon + 1, sizeof((yyval.cmd)->state)); /* dst path */ - } - free((yyvsp[(3) - (4)].str)); - free((yyvsp[(4) - (4)].str)); - break; - } case 158: #line 1257 "test_spec_parse.y" - { - (yyval.cmd) = make_cmd(CMD_STOP_POSTGRES); - strlcpy((yyval.cmd)->service, (yyvsp[(3) - (3)].str), - sizeof((yyval.cmd)->service)); - free((yyvsp[(3) - (3)].str)); - break; - } + { + (yyval.cmd) = make_cmd(CMD_STOP_POSTGRES); + strlcpy((yyval.cmd)->service, (yyvsp[(3) - (3)].str), + sizeof((yyval.cmd)->service)); + free((yyvsp[(3) - (3)].str)); + break; + } case 159: #line 1263 "test_spec_parse.y" - { - (yyval.cmd) = make_cmd(CMD_START_POSTGRES); - strlcpy((yyval.cmd)->service, (yyvsp[(3) - (3)].str), - sizeof((yyval.cmd)->service)); - free((yyvsp[(3) - (3)].str)); - break; - } + { + (yyval.cmd) = make_cmd(CMD_START_POSTGRES); + strlcpy((yyval.cmd)->service, (yyvsp[(3) - (3)].str), + sizeof((yyval.cmd)->service)); + free((yyvsp[(3) - (3)].str)); + break; + } case 160: #line 1279 "test_spec_parse.y" - { - pgaf_next_brace_is_while = 1; - break; - } + { + pgaf_next_brace_is_while = 1; + break; + } case 161: #line 1280 "test_spec_parse.y" - { - (yyval.step) = (yyvsp[(4) - (5)].step); - break; - } + { + (yyval.step) = (yyvsp[(4) - (5)].step); + break; + } case 162: #line 1285 "test_spec_parse.y" - { - (yyval.cmd) = make_cmd(CMD_STAYS_WHILE); - strlcpy((yyval.cmd)->service, (yyvsp[(2) - (5)].str), - sizeof((yyval.cmd)->service)); - strlcpy((yyval.cmd)->state, (yyvsp[(4) - (5)].str), - sizeof((yyval.cmd)->state)); - (yyval.cmd)->body = ((yyvsp[(5) - (5)].step)) ? (yyvsp[(5) - - (5)].step)-> - commands : NULL; - free((yyvsp[(2) - (5)].str)); - break; - } + { + (yyval.cmd) = make_cmd(CMD_STAYS_WHILE); + strlcpy((yyval.cmd)->service, (yyvsp[(2) - (5)].str), + sizeof((yyval.cmd)->service)); + strlcpy((yyval.cmd)->state, (yyvsp[(4) - (5)].str), + sizeof((yyval.cmd)->state)); + (yyval.cmd)->body = ((yyvsp[(5) - (5)].step)) ? (yyvsp[(5) - + (5)].step)-> + commands : NULL; + free((yyvsp[(2) - (5)].str)); + break; + } case 163: #line 1304 "test_spec_parse.y" - { - /* only "set monitor " is supported; $2 must be "monitor" */ - if (strcmp((yyvsp[(2) - (3)].str), "monitor") != 0) - { - fprintf(/* IGNORE-BANNED */ stderr, - "pgaftest: unknown 'set' target '%s' (expected 'monitor')\n", - (yyvsp[(2) - - ( - 3)].str)); + { + /* only "set monitor " is supported; $2 must be "monitor" */ + if (strcmp((yyvsp[(2) - (3)].str), "monitor") != 0) + { + fprintf(/* IGNORE-BANNED */ stderr, + "pgaftest: unknown 'set' target '%s' (expected 'monitor')\n", + (yyvsp[(2) - + ( + 3)].str)); + free((yyvsp[(2) - (3)].str)); + free((yyvsp[(3) - (3)].str)); + YYERROR; + } + (yyval.cmd) = make_cmd(CMD_SET_MONITOR); + strlcpy((yyval.cmd)->service, (yyvsp[(3) - (3)].str), + sizeof((yyval.cmd)->service)); free((yyvsp[(2) - (3)].str)); free((yyvsp[(3) - (3)].str)); - YYERROR; + break; } - (yyval.cmd) = make_cmd(CMD_SET_MONITOR); - strlcpy((yyval.cmd)->service, (yyvsp[(3) - (3)].str), - sizeof((yyval.cmd)->service)); - free((yyvsp[(2) - (3)].str)); - free((yyvsp[(3) - (3)].str)); - break; - } case 164: #line 1329 "test_spec_parse.y" - { - (yyval.cmd) = make_cmd(CMD_LOGS_CHECK); - strlcpy((yyval.cmd)->service, (yyvsp[(2) - (4)].str), - sizeof((yyval.cmd)->service)); - strlcpy((yyval.cmd)->args, (yyvsp[(4) - (4)].str), - sizeof((yyval.cmd)->args)); - (yyval.cmd)->logsNegate = false; - (yyval.cmd)->allowError = false; /* false = fixed string, true = PCRE */ - free((yyvsp[(2) - (4)].str)); - free((yyvsp[(4) - (4)].str)); - break; - } + { + (yyval.cmd) = make_cmd(CMD_LOGS_CHECK); + strlcpy((yyval.cmd)->service, (yyvsp[(2) - (4)].str), + sizeof((yyval.cmd)->service)); + strlcpy((yyval.cmd)->args, (yyvsp[(4) - (4)].str), + sizeof((yyval.cmd)->args)); + (yyval.cmd)->logsNegate = false; + (yyval.cmd)->allowError = false; /* false = fixed string, true = PCRE */ + free((yyvsp[(2) - (4)].str)); + free((yyvsp[(4) - (4)].str)); + break; + } case 165: #line 1338 "test_spec_parse.y" - { - (yyval.cmd) = make_cmd(CMD_LOGS_CHECK); - strlcpy((yyval.cmd)->service, (yyvsp[(2) - (5)].str), - sizeof((yyval.cmd)->service)); - strlcpy((yyval.cmd)->args, (yyvsp[(5) - (5)].str), - sizeof((yyval.cmd)->args)); - (yyval.cmd)->logsNegate = true; - (yyval.cmd)->allowError = false; - free((yyvsp[(2) - (5)].str)); - free((yyvsp[(5) - (5)].str)); - break; - } + { + (yyval.cmd) = make_cmd(CMD_LOGS_CHECK); + strlcpy((yyval.cmd)->service, (yyvsp[(2) - (5)].str), + sizeof((yyval.cmd)->service)); + strlcpy((yyval.cmd)->args, (yyvsp[(5) - (5)].str), + sizeof((yyval.cmd)->args)); + (yyval.cmd)->logsNegate = true; + (yyval.cmd)->allowError = false; + free((yyvsp[(2) - (5)].str)); + free((yyvsp[(5) - (5)].str)); + break; + } case 166: #line 1347 "test_spec_parse.y" - { - (yyval.cmd) = make_cmd(CMD_LOGS_CHECK); - strlcpy((yyval.cmd)->service, (yyvsp[(2) - (4)].str), - sizeof((yyval.cmd)->service)); - strlcpy((yyval.cmd)->args, (yyvsp[(4) - (4)].str), - sizeof((yyval.cmd)->args)); - (yyval.cmd)->logsNegate = false; - (yyval.cmd)->allowError = true; /* true = PCRE (-P) */ - free((yyvsp[(2) - (4)].str)); - free((yyvsp[(4) - (4)].str)); - break; - } + { + (yyval.cmd) = make_cmd(CMD_LOGS_CHECK); + strlcpy((yyval.cmd)->service, (yyvsp[(2) - (4)].str), + sizeof((yyval.cmd)->service)); + strlcpy((yyval.cmd)->args, (yyvsp[(4) - (4)].str), + sizeof((yyval.cmd)->args)); + (yyval.cmd)->logsNegate = false; + (yyval.cmd)->allowError = true; /* true = PCRE (-P) */ + free((yyvsp[(2) - (4)].str)); + free((yyvsp[(4) - (4)].str)); + break; + } case 167: #line 1356 "test_spec_parse.y" - { - (yyval.cmd) = make_cmd(CMD_LOGS_CHECK); - strlcpy((yyval.cmd)->service, (yyvsp[(2) - (5)].str), - sizeof((yyval.cmd)->service)); - strlcpy((yyval.cmd)->args, (yyvsp[(5) - (5)].str), - sizeof((yyval.cmd)->args)); - (yyval.cmd)->logsNegate = true; - (yyval.cmd)->allowError = true; - free((yyvsp[(2) - (5)].str)); - free((yyvsp[(5) - (5)].str)); - break; - } + { + (yyval.cmd) = make_cmd(CMD_LOGS_CHECK); + strlcpy((yyval.cmd)->service, (yyvsp[(2) - (5)].str), + sizeof((yyval.cmd)->service)); + strlcpy((yyval.cmd)->args, (yyvsp[(5) - (5)].str), + sizeof((yyval.cmd)->args)); + (yyval.cmd)->logsNegate = true; + (yyval.cmd)->allowError = true; + free((yyvsp[(2) - (5)].str)); + free((yyvsp[(5) - (5)].str)); + break; + } case 170: #line 1377 "test_spec_parse.y" - { - int i = current_spec->sequenceLength; - if (i < PGAF_MAX_SEQ) { - current_spec->sequence[current_spec->sequenceLength++] = (yyvsp[(2) - - (2)]. - str); - } - else - { - fprintf(/* IGNORE-BANNED */ stderr, - "pgaftest: too many steps in sequence (max %d)\n", - PGAF_MAX_SEQ); - exit(1); + int i = current_spec->sequenceLength; + if (i < PGAF_MAX_SEQ) + { + current_spec->sequence[current_spec->sequenceLength++] = (yyvsp[(2) - + (2)]. + str); + } + else + { + fprintf(/* IGNORE-BANNED */ stderr, + "pgaftest: too many steps in sequence (max %d)\n", + PGAF_MAX_SEQ); + exit(1); + } + break; } - break; - } case 171: #line 1398 "test_spec_parse.y" - { - (yyval.str) = "init"; - break; - } + { + (yyval.str) = "init"; + break; + } case 172: #line 1399 "test_spec_parse.y" - { - (yyval.str) = "single"; - break; - } + { + (yyval.str) = "single"; + break; + } case 173: #line 1400 "test_spec_parse.y" - { - (yyval.str) = "primary"; - break; - } + { + (yyval.str) = "primary"; + break; + } case 174: #line 1401 "test_spec_parse.y" - { - (yyval.str) = "wait_primary"; - break; - } + { + (yyval.str) = "wait_primary"; + break; + } case 175: #line 1402 "test_spec_parse.y" - { - (yyval.str) = "wait_standby"; - break; - } + { + (yyval.str) = "wait_standby"; + break; + } case 176: #line 1403 "test_spec_parse.y" - { - (yyval.str) = "demoted"; - break; - } + { + (yyval.str) = "demoted"; + break; + } case 177: #line 1404 "test_spec_parse.y" - { - (yyval.str) = "demote_timeout"; - break; - } + { + (yyval.str) = "demote_timeout"; + break; + } case 178: #line 1405 "test_spec_parse.y" - { - (yyval.str) = "draining"; - break; - } + { + (yyval.str) = "draining"; + break; + } case 179: #line 1406 "test_spec_parse.y" - { - (yyval.str) = "secondary"; - break; - } + { + (yyval.str) = "secondary"; + break; + } case 180: #line 1407 "test_spec_parse.y" - { - (yyval.str) = "catchingup"; - break; - } + { + (yyval.str) = "catchingup"; + break; + } case 181: #line 1408 "test_spec_parse.y" - { - (yyval.str) = "prepare_promotion"; - break; - } + { + (yyval.str) = "prepare_promotion"; + break; + } case 182: #line 1409 "test_spec_parse.y" - { - (yyval.str) = "stop_replication"; - break; - } + { + (yyval.str) = "stop_replication"; + break; + } case 183: #line 1410 "test_spec_parse.y" - { - (yyval.str) = "maintenance"; - break; - } + { + (yyval.str) = "maintenance"; + break; + } case 184: #line 1411 "test_spec_parse.y" - { - (yyval.str) = "join_primary"; - break; - } + { + (yyval.str) = "join_primary"; + break; + } case 185: #line 1412 "test_spec_parse.y" - { - (yyval.str) = "apply_settings"; - break; - } + { + (yyval.str) = "apply_settings"; + break; + } case 186: #line 1413 "test_spec_parse.y" - { - (yyval.str) = "prepare_maintenance"; - break; - } + { + (yyval.str) = "prepare_maintenance"; + break; + } case 187: #line 1414 "test_spec_parse.y" - { - (yyval.str) = "wait_maintenance"; - break; - } + { + (yyval.str) = "wait_maintenance"; + break; + } case 188: #line 1415 "test_spec_parse.y" - { - (yyval.str) = "report_lsn"; - break; - } + { + (yyval.str) = "report_lsn"; + break; + } case 189: #line 1416 "test_spec_parse.y" - { - (yyval.str) = "fast_forward"; - break; - } + { + (yyval.str) = "fast_forward"; + break; + } case 190: #line 1417 "test_spec_parse.y" - { - (yyval.str) = "join_secondary"; - break; - } + { + (yyval.str) = "join_secondary"; + break; + } case 191: #line 1418 "test_spec_parse.y" - { - (yyval.str) = "dropped"; - break; - } + { + (yyval.str) = "dropped"; + break; + } case 192: #line 1426 "test_spec_parse.y" - { - (yyval.str) = (yyvsp[(1) - (1)].str); - break; - } + { + (yyval.str) = (yyvsp[(1) - (1)].str); + break; + } case 193: #line 1427 "test_spec_parse.y" - { - (yyval.str) = (yyvsp[(1) - (1)].str); - break; - } + { + (yyval.str) = (yyvsp[(1) - (1)].str); + break; + } /* Line 1267 of yacc.c. */ @@ -4011,8 +4011,8 @@ parse_test_spec(const char *filename) if (!f) { fprintf(/* IGNORE-BANNED */ stderr, - "pgaftest: cannot open spec file \"%s\": %s\n", - filename, strerror(errno) /* IGNORE-BANNED */); + "pgaftest: cannot open spec file \"%s\": %s\n", + filename, strerror(errno) /* IGNORE-BANNED */); return NULL; } diff --git a/src/bin/pgaftest/test_spec_scan.c b/src/bin/pgaftest/test_spec_scan.c index c51cde914..09618b246 100644 --- a/src/bin/pgaftest/test_spec_scan.c +++ b/src/bin/pgaftest/test_spec_scan.c @@ -173,17 +173,17 @@ extern FILE *yyin, *yyout; /* Return all but the first "n" matched characters back to the input stream. */ #define yyless(n) \ - do \ - { \ - /* Undo effects of setting up yytext. */ \ - int yyless_macro_arg = (n); \ - YY_LESS_LINENO(yyless_macro_arg); \ - *yy_cp = (yy_hold_char); \ - YY_RESTORE_YY_MORE_OFFSET \ - (yy_c_buf_p) = yy_cp = yy_bp + yyless_macro_arg - YY_MORE_ADJ; \ - YY_DO_BEFORE_ACTION; /* set up yytext again */ \ - } \ - while (0) + do \ + { \ + /* Undo effects of setting up yytext. */ \ + int yyless_macro_arg = (n); \ + YY_LESS_LINENO(yyless_macro_arg); \ + *yy_cp = (yy_hold_char); \ + YY_RESTORE_YY_MORE_OFFSET \ + (yy_c_buf_p) = yy_cp = yy_bp + yyless_macro_arg - YY_MORE_ADJ; \ + YY_DO_BEFORE_ACTION; /* set up yytext again */ \ + } \ + while (0) #define unput(c) yyunput(c, (yytext_ptr)) #ifndef YY_STRUCT_YY_BUFFER_STATE @@ -309,23 +309,23 @@ void yyfree(void *); #define yy_new_buffer yy_create_buffer #define yy_set_interactive(is_interactive) \ - { \ - if (!YY_CURRENT_BUFFER) { \ - yyensure_buffer_stack(); \ - YY_CURRENT_BUFFER_LVALUE = \ - yy_create_buffer(yyin, YY_BUF_SIZE); \ - } \ - YY_CURRENT_BUFFER_LVALUE->yy_is_interactive = is_interactive; \ - } + { \ + if (!YY_CURRENT_BUFFER) { \ + yyensure_buffer_stack(); \ + YY_CURRENT_BUFFER_LVALUE = \ + yy_create_buffer(yyin, YY_BUF_SIZE); \ + } \ + YY_CURRENT_BUFFER_LVALUE->yy_is_interactive = is_interactive; \ + } #define yy_set_bol(at_bol) \ - { \ - if (!YY_CURRENT_BUFFER) { \ - yyensure_buffer_stack(); \ - YY_CURRENT_BUFFER_LVALUE = \ - yy_create_buffer(yyin, YY_BUF_SIZE); \ - } \ - YY_CURRENT_BUFFER_LVALUE->yy_at_bol = at_bol; \ - } + { \ + if (!YY_CURRENT_BUFFER) { \ + yyensure_buffer_stack(); \ + YY_CURRENT_BUFFER_LVALUE = \ + yy_create_buffer(yyin, YY_BUF_SIZE); \ + } \ + YY_CURRENT_BUFFER_LVALUE->yy_at_bol = at_bol; \ + } #define YY_AT_BOL() (YY_CURRENT_BUFFER_LVALUE->yy_at_bol) /* Begin user sect3 */ @@ -356,11 +356,11 @@ static void yynoreturn yy_fatal_error(const char *msg); * corresponding action - sets up yytext. */ #define YY_DO_BEFORE_ACTION \ - (yytext_ptr) = yy_bp; \ - yyleng = (yy_size_t) (yy_cp - yy_bp); \ - (yy_hold_char) = *yy_cp; \ - *yy_cp = '\0'; \ - (yy_c_buf_p) = yy_cp; + (yytext_ptr) = yy_bp; \ + yyleng = (yy_size_t) (yy_cp - yy_bp); \ + (yy_hold_char) = *yy_cp; \ + *yy_cp = '\0'; \ + (yy_c_buf_p) = yy_cp; #define YY_NUM_RULES 147 #define YY_END_OF_BUFFER 148 @@ -1320,35 +1320,35 @@ static int input(void); */ #ifndef YY_INPUT #define YY_INPUT(buf, result, max_size) \ - if (YY_CURRENT_BUFFER_LVALUE->yy_is_interactive) \ - { \ - int c = '*'; \ - yy_size_t n; \ - for (n = 0; n < max_size && \ - (c = getc(yyin)) != EOF && c != '\n'; ++n) { \ - buf[n] = (char) c; } \ - if (c == '\n') { \ - buf[n++] = (char) c; } \ - if (c == EOF && ferror(yyin)) { \ - YY_FATAL_ERROR("input in flex scanner failed"); } \ - result = n; \ - } \ - else \ + if (YY_CURRENT_BUFFER_LVALUE->yy_is_interactive) \ + { \ + int c = '*'; \ + yy_size_t n; \ + for (n = 0; n < max_size && \ + (c = getc(yyin)) != EOF && c != '\n'; ++n) { \ + buf[n] = (char) c; } \ + if (c == '\n') { \ + buf[n++] = (char) c; } \ + if (c == EOF && ferror(yyin)) { \ + YY_FATAL_ERROR("input in flex scanner failed"); } \ + result = n; \ + } \ + else \ + { \ + errno = 0; \ + while ((result = (int) fread(buf, 1, (yy_size_t) max_size, yyin)) == 0 && \ + ferror( \ + yyin)) \ { \ - errno = 0; \ - while ((result = (int) fread(buf, 1, (yy_size_t) max_size, yyin)) == 0 && \ - ferror( \ - yyin)) \ + if (errno != EINTR) \ { \ - if (errno != EINTR) \ - { \ - YY_FATAL_ERROR("input in flex scanner failed"); \ - break; \ - } \ - errno = 0; \ - clearerr(yyin); \ + YY_FATAL_ERROR("input in flex scanner failed"); \ + break; \ } \ + errno = 0; \ + clearerr(yyin); \ } \ + } \ \ #endif @@ -1397,11 +1397,11 @@ extern int yylex(void); #endif #define YY_RULE_SETUP \ - YY_USER_ACTION + YY_USER_ACTION /** The main scanner function which does all the work. */ - YY_DECL +YY_DECL { yy_state_type yy_current_state; char *yy_cp, *yy_bp; @@ -1681,8 +1681,8 @@ extern int yylex(void); /* fallback: should not happen in a well-formed file */ fprintf(/* IGNORE-BANNED */ stderr, - "pgaftest: unexpected '{' at line %d\n", - pgaf_line_number); + "pgaftest: unexpected '{' at line %d\n", + pgaf_line_number); } YY_BREAK @@ -1716,8 +1716,8 @@ extern int yylex(void); #line 143 "test_spec_scan.l" { fprintf(/* IGNORE-BANNED */ stderr, - "pgaftest: unexpected character '%c' at line %d\n", - yytext[0], pgaf_line_number); + "pgaftest: unexpected character '%c' at line %d\n", + yytext[0], pgaf_line_number); } YY_BREAK @@ -4080,18 +4080,18 @@ yy_fatal_error(const char *msg) #undef yyless #define yyless(n) \ - do \ - { \ - /* Undo effects of setting up yytext. */ \ - yy_size_t yyless_macro_arg = (n); \ - YY_LESS_LINENO(yyless_macro_arg); \ - yytext[yyleng] = (yy_hold_char); \ - (yy_c_buf_p) = yytext + yyless_macro_arg; \ - (yy_hold_char) = *(yy_c_buf_p); \ - *(yy_c_buf_p) = '\0'; \ - yyleng = yyless_macro_arg; \ - } \ - while (0) + do \ + { \ + /* Undo effects of setting up yytext. */ \ + yy_size_t yyless_macro_arg = (n); \ + YY_LESS_LINENO(yyless_macro_arg); \ + yytext[yyleng] = (yy_hold_char); \ + (yy_c_buf_p) = yytext + yyless_macro_arg; \ + (yy_hold_char) = *(yy_c_buf_p); \ + *(yy_c_buf_p) = '\0'; \ + yyleng = yyless_macro_arg; \ + } \ + while (0) /* Accessor methods (get/set functions) to struct members. */ diff --git a/src/monitor/health_check_worker.c b/src/monitor/health_check_worker.c index 36dc0fe5d..b19f7fa6a 100644 --- a/src/monitor/health_check_worker.c +++ b/src/monitor/health_check_worker.c @@ -52,9 +52,9 @@ * that TLS is not necessarily used, because no secret information is sent. */ #define CONN_INFO_TEMPLATE \ - "host=%s port=%u user=pgautofailover_monitor " \ - "password=pgautofailover_monitor dbname=postgres " \ - "connect_timeout=%u" + "host=%s port=%u user=pgautofailover_monitor " \ + "password=pgautofailover_monitor dbname=postgres " \ + "connect_timeout=%u" #define MAX_CONN_INFO_SIZE 1024 diff --git a/src/monitor/node_metadata.h b/src/monitor/node_metadata.h index 62280b411..607cf8bd9 100644 --- a/src/monitor/node_metadata.h +++ b/src/monitor/node_metadata.h @@ -50,31 +50,31 @@ #define Anum_pgautofailover_node_nodecluster 21 #define AUTO_FAILOVER_NODE_TABLE_ALL_COLUMNS \ - "formationid, " \ - "nodeid, " \ - "groupid, " \ - "nodename, " \ - "nodehost, " \ - "nodeport, " \ - "sysidentifier, " \ - "goalstate, " \ - "reportedstate, " \ - "reportedpgisrunning, " \ - "reportedrepstate, " \ - "reporttime, " \ - "reportedtli, " \ - "reportedlsn, " \ - "walreporttime, " \ - "health, " \ - "healthchecktime, " \ - "statechangetime, " \ - "candidatepriority, " \ - "replicationquorum, " \ - "nodecluster" + "formationid, " \ + "nodeid, " \ + "groupid, " \ + "nodename, " \ + "nodehost, " \ + "nodeport, " \ + "sysidentifier, " \ + "goalstate, " \ + "reportedstate, " \ + "reportedpgisrunning, " \ + "reportedrepstate, " \ + "reporttime, " \ + "reportedtli, " \ + "reportedlsn, " \ + "walreporttime, " \ + "health, " \ + "healthchecktime, " \ + "statechangetime, " \ + "candidatepriority, " \ + "replicationquorum, " \ + "nodecluster" #define SELECT_ALL_FROM_AUTO_FAILOVER_NODE_TABLE \ - "SELECT " AUTO_FAILOVER_NODE_TABLE_ALL_COLUMNS " FROM " AUTO_FAILOVER_NODE_TABLE + "SELECT " AUTO_FAILOVER_NODE_TABLE_ALL_COLUMNS " FROM " AUTO_FAILOVER_NODE_TABLE /* pg_stat_replication.sync_state: "sync", "async", "quorum", "potential" */ typedef enum SyncState @@ -104,7 +104,7 @@ typedef enum SyncState */ #define NODE_FORMAT "node %lld \"%s\" (%s:%d)" #define NODE_FORMAT_ARGS(node) \ - (long long) node->nodeId, node->nodeName, node->nodeHost, node->nodePort + (long long) node->nodeId, node->nodeName, node->nodeHost, node->nodePort /* * AutoFailoverNode represents a Postgres node that is being tracked by the diff --git a/src/monitor/version_compat.h b/src/monitor/version_compat.h index f11dfb019..82f36abc0 100644 --- a/src/monitor/version_compat.h +++ b/src/monitor/version_compat.h @@ -27,10 +27,10 @@ #define DEFAULT_XLOG_SEG_SIZE XLOG_SEG_SIZE #define BackgroundWorkerInitializeConnection(dbname, username, flags) \ - BackgroundWorkerInitializeConnection(dbname, username) + BackgroundWorkerInitializeConnection(dbname, username) #define BackgroundWorkerInitializeConnectionByOid(dboid, useroid, flags) \ - BackgroundWorkerInitializeConnectionByOid(dboid, useroid) + BackgroundWorkerInitializeConnectionByOid(dboid, useroid) #include "nodes/pg_list.h" From 7f2ee8182b83bf498c4d5631d7338c7b3e7ae439 Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Tue, 7 Jul 2026 15:50:56 +0200 Subject: [PATCH 33/68] restore: use original implementations from pgaftest-infra branch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace reinvented C implementations with the already-tested versions from the original pgaftest-infra branch (origin/pgaftest-infra): - cli_do_misc.c: restore proper keeper_cli_pgsetup_wait_getopts that delegates to keeper_cli_keeper_setup_getopts after stripping --timeout and --read-write (handles PGDATA env var correctly); restore full wait_until_ready with config-file wait loop and --read-write phase - cli_do_root.c/h: restore correct --read-write/--timeout help text and hba-lan command description - cli_common.c: restore monitor_password (case 'W') and replication_password (case 'w') in cli_common_keeper_getopts - cli_create_node.c: restore correct option letter assignments and --formation support in monitor create getopts - cli_drop_node.c: restore --no-wait implementation - keeper_config.h/c: restore monitor_password field and ini option - monitor_config.h/c: restore autoctl_node_password field and ini option - monitor_pg_init.h/c: restore autoctl_node_password parameter - service_monitor_init.c: restore autoctl_node_password call - nodespec.c/h: restore full monitor_uri change handling and password argv building Also update workflow to use schedule-based matrix (76→24 jobs) and add tests/tap/schedules/ directory. --- .github/workflows/run-pgaftest.yml | 42 ++-- src/bin/pg_autoctl/cli_common.c | 45 ++-- src/bin/pg_autoctl/cli_create_node.c | 33 ++- src/bin/pg_autoctl/cli_do_misc.c | 254 ++++++++++++++----- src/bin/pg_autoctl/cli_do_root.c | 286 ++++++++++++++++++---- src/bin/pg_autoctl/cli_do_root.h | 47 ++-- src/bin/pg_autoctl/cli_drop_node.c | 20 +- src/bin/pg_autoctl/keeper_config.c | 6 + src/bin/pg_autoctl/keeper_config.h | 1 + src/bin/pg_autoctl/monitor_config.c | 9 +- src/bin/pg_autoctl/monitor_config.h | 8 +- src/bin/pg_autoctl/monitor_pg_init.c | 29 ++- src/bin/pg_autoctl/nodespec.c | 133 +--------- src/bin/pg_autoctl/nodespec.h | 4 +- src/bin/pg_autoctl/service_monitor_init.c | 21 ++ tests/tap/schedules/monitor.sch | 4 + tests/tap/schedules/multi-1.sch | 4 + tests/tap/schedules/multi-2.sch | 4 + tests/tap/schedules/node.sch | 4 + tests/tap/schedules/quick.sch | 5 + tests/tap/schedules/ssl.sch | 4 + 21 files changed, 639 insertions(+), 324 deletions(-) create mode 100644 tests/tap/schedules/monitor.sch create mode 100644 tests/tap/schedules/multi-1.sch create mode 100644 tests/tap/schedules/multi-2.sch create mode 100644 tests/tap/schedules/node.sch create mode 100644 tests/tap/schedules/quick.sch create mode 100644 tests/tap/schedules/ssl.sch diff --git a/.github/workflows/run-pgaftest.yml b/.github/workflows/run-pgaftest.yml index 097eae6d7..938d5a2f2 100644 --- a/.github/workflows/run-pgaftest.yml +++ b/.github/workflows/run-pgaftest.yml @@ -103,38 +103,26 @@ jobs: retention-days: 1 # --------------------------------------------------------------------------- - # Run every spec against every supported Postgres version. - # Each job downloads its matching pgaf:run-pgNN image and the shared binary. + # Run test schedules against every supported Postgres version. + # Each schedule groups several specs to reduce total job count from + # 19-specs × 4-PG = 76 jobs down to 6-schedules × 4-PG = 24 jobs. # --------------------------------------------------------------------------- test: - name: pgaftest / ${{ matrix.spec }} (PG${{ matrix.PGVERSION }}) + name: pgaftest / ${{ matrix.schedule }} (PG${{ matrix.PGVERSION }}) needs: [build-images, build-pgaftest] runs-on: ubuntu-latest - timeout-minutes: 25 + timeout-minutes: 40 strategy: fail-fast: false matrix: PGVERSION: [14, 15, 16, 17] - spec: - - basic_operation - - basic_operation_listen_flag - - maintenance_and_drop - - create_standby_with_pgdata - - ensure - - monitor_disabled - - replace_monitor - - config_get_set - - skip_pg_hba - - auth - - enable_ssl - - ssl_cert - - ssl_self_signed - - multi_standbys - - multi_async - - multi_ifdown - - multi_maintenance - - multi_alternate - - extension_update + schedule: + - quick # basic_operation, basic_operation_listen_flag, config_get_set, skip_pg_hba + - node # create_standby_with_pgdata, maintenance_and_drop, auth + - monitor # monitor_disabled, replace_monitor, extension_update + - ssl # enable_ssl, ssl_self_signed, ssl_cert + - multi-1 # multi_standbys, multi_maintenance, multi_async + - multi-2 # ensure, multi_alternate, multi_ifdown steps: - uses: actions/checkout@v7.0.0 @@ -168,13 +156,13 @@ jobs: sudo apt-get update sudo apt-get install -y libpq5 - - name: Run ${{ matrix.spec }} - timeout-minutes: 18 + - name: Run schedule ${{ matrix.schedule }} + timeout-minutes: 35 env: PGAF_IMAGE: pgaf:run run: | chmod +x /tmp/pgaftest-bin - /tmp/pgaftest-bin run tests/tap/specs/${{ matrix.spec }}.pgaf + /tmp/pgaftest-bin run --schedule tests/tap/schedules/${{ matrix.schedule }}.sch # --------------------------------------------------------------------------- # installcheck: SQL regression suite via pg_regress. diff --git a/src/bin/pg_autoctl/cli_common.c b/src/bin/pg_autoctl/cli_common.c index 794c00ae8..898b7bf5c 100644 --- a/src/bin/pg_autoctl/cli_common.c +++ b/src/bin/pg_autoctl/cli_common.c @@ -407,28 +407,6 @@ cli_common_keeper_getopts(int argc, char **argv, break; } - case 'e': - { - /* { "replication-password", required_argument, NULL, 'e' } */ - strlcpy(LocalOptionConfig.replication_password, optarg, - MAXCONNINFO); - log_trace("--replication-password ****"); - break; - } - - case 'w': - { - /* - * { "monitor-password", required_argument, NULL, 'w' } - * The pgautofailover_monitor health-check role currently uses a - * hardcoded password (PG_AUTOCTL_HEALTH_PASSWORD). Accept the - * option so pg_autoctl node run can pass it without error; it - * is otherwise unused at this time. - */ - log_trace("--monitor-password ****"); - break; - } - case 'V': { /* keeper_cli_print_version prints version and exits. */ @@ -517,6 +495,24 @@ cli_common_keeper_getopts(int argc, char **argv, break; } + case 'W': + { + /* { "monitor-password", required_argument, NULL, 'W' } */ + strlcpy(LocalOptionConfig.monitor_password, optarg, + sizeof(LocalOptionConfig.monitor_password)); + log_trace("--monitor-password ****"); + break; + } + + case 'w': + { + /* { "replication-password", required_argument, NULL, 'w' } */ + strlcpy(LocalOptionConfig.replication_password, optarg, + sizeof(LocalOptionConfig.replication_password)); + log_trace("--replication-password ****"); + break; + } + /* * { "ssl-ca-file", required_argument, &ssl_flag, SSL_CA_FILE_FLAG } * { "ssl-crl-file", required_argument, &ssl_flag, SSL_CA_FILE_FLAG } @@ -1444,6 +1440,11 @@ keeper_cli_help(int argc, char **argv) { CommandLine command = root; + if (env_exists(PG_AUTOCTL_DEBUG)) + { + command = root_with_debug; + } + (void) commandline_print_command_tree(&command, stdout); } diff --git a/src/bin/pg_autoctl/cli_create_node.c b/src/bin/pg_autoctl/cli_create_node.c index 4b870c218..c8d9c8a02 100644 --- a/src/bin/pg_autoctl/cli_create_node.c +++ b/src/bin/pg_autoctl/cli_create_node.c @@ -334,6 +334,8 @@ cli_create_postgres_getopts(int argc, char **argv) { "monitor", required_argument, NULL, 'm' }, { "disable-monitor", no_argument, NULL, 'M' }, { "node-id", required_argument, NULL, 'I' }, + { "monitor-password", required_argument, NULL, 'W' }, + { "replication-password", required_argument, NULL, 'w' }, { "version", no_argument, NULL, 'V' }, { "verbose", no_argument, NULL, 'v' }, { "quiet", no_argument, NULL, 'q' }, @@ -341,8 +343,6 @@ cli_create_postgres_getopts(int argc, char **argv) { "candidate-priority", required_argument, NULL, 'P' }, { "replication-quorum", required_argument, NULL, 'r' }, { "maximum-backup-rate", required_argument, NULL, 'R' }, - { "replication-password", required_argument, NULL, 'e' }, - { "monitor-password", required_argument, NULL, 'w' }, { "run", no_argument, NULL, 'x' }, { "no-ssl", no_argument, NULL, 'N' }, { "ssl-self-signed", no_argument, NULL, 's' }, @@ -356,7 +356,7 @@ cli_create_postgres_getopts(int argc, char **argv) int optind = cli_create_node_getopts(argc, argv, long_options, - "C:D:H:p:l:U:A:SLd:a:n:f:m:MI:Re:w:VvqhP:r:xsN", + "C:D:H:p:l:U:A:SLd:a:n:f:m:MI:W:w:RVvqhP:r:xsN", &options); /* publish our option parsing in the global variable */ @@ -795,7 +795,8 @@ cli_create_monitor_getopts(int argc, char **argv) { "listen", required_argument, NULL, 'l' }, { "auth", required_argument, NULL, 'A' }, { "skip-pg-hba", no_argument, NULL, 'S' }, - { "autoctl-node-password", required_argument, NULL, 'e' }, + { "autoctl-node-password", required_argument, NULL, 'W' }, + { "formation", required_argument, NULL, 'f' }, { "version", no_argument, NULL, 'V' }, { "verbose", no_argument, NULL, 'v' }, { "quiet", no_argument, NULL, 'q' }, @@ -824,7 +825,7 @@ cli_create_monitor_getopts(int argc, char **argv) optind = 0; - while ((c = getopt_long(argc, argv, "C:D:p:n:l:A:Se:VvqhxNs", + while ((c = getopt_long(argc, argv, "C:D:p:n:l:A:SW:f:VvqhxsN", long_options, &option_index)) != -1) { switch (c) @@ -901,14 +902,30 @@ cli_create_monitor_getopts(int argc, char **argv) break; } - case 'e': + case 'W': { - /* { "autoctl-node-password", required_argument, NULL, 'e' } */ - strlcpy(options.autoctl_node_password, optarg, MAXCONNINFO); + strlcpy(options.autoctl_node_password, optarg, + sizeof(options.autoctl_node_password)); log_trace("--autoctl-node-password ****"); break; } + case 'f': + { + /* --formation (may be repeated) */ + if (options.formationCount >= MONITOR_MAX_FORMATIONS) + { + log_error("Too many --formation options (max %d)", + MONITOR_MAX_FORMATIONS); + errors++; + break; + } + int fi = options.formationCount++; + strlcpy(options.formationNames[fi], optarg, NAMEDATALEN); + log_trace("--formation %s", optarg); + break; + } + case 'V': { /* keeper_cli_print_version prints version and exits. */ diff --git a/src/bin/pg_autoctl/cli_do_misc.c b/src/bin/pg_autoctl/cli_do_misc.c index 377e7f75f..68e144a95 100644 --- a/src/bin/pg_autoctl/cli_do_misc.c +++ b/src/bin/pg_autoctl/cli_do_misc.c @@ -38,6 +38,10 @@ #include "primary_standby.h" #include "string_utils.h" +/* Options specific to "pg_autoctl inspect pgsetup wait" */ +static bool pgsetupWaitReadWrite = false; +static int pgsetupWaitTimeout = 30; + /* * keeper_cli_create_replication_slot implements the CLI to create a replication @@ -322,109 +326,150 @@ keeper_cli_pgsetup_is_ready(int argc, char **argv) } -/* timeout parsed by keeper_cli_pgsetup_wait_getopts, consumed by wait_until_ready */ -static int pgsetup_wait_timeout = 30; - /* - * keeper_cli_pgsetup_wait_getopts parses --pgdata and --timeout for the - * "pgsetup wait" subcommand. + * keeper_cli_pgsetup_wait_getopts parses options specific to + * "pg_autoctl inspect pgsetup wait": --read-write, --timeout, plus the + * standard Postgres connection options inherited from the keeper setup. */ int keeper_cli_pgsetup_wait_getopts(int argc, char **argv) { - int c, option_index = 0; - - static struct option long_options[] = { - { "pgdata", required_argument, NULL, 'D' }, - { "timeout", required_argument, NULL, 't' }, - { "version", no_argument, NULL, 'V' }, - { "verbose", no_argument, NULL, 'v' }, - { "quiet", no_argument, NULL, 'q' }, - { "help", no_argument, NULL, 'h' }, + /* + * cli_common_keeper_getopts (called by keeper_cli_keeper_setup_getopts) + * exits with BAD_ARGS when it encounters unknown options. Since --timeout + * and --read-write are not in its options list, we must remove them from + * argv before delegating. + * + * Strategy: + * 1. Scan argv with opterr=0 to capture --timeout / --read-write. + * 2. Build a filtered argv that omits those two options. + * 3. Pass the filtered argv to keeper_cli_keeper_setup_getopts. + */ + + /* Reset module-level wait options */ + pgsetupWaitReadWrite = false; + pgsetupWaitTimeout = 30; + + static struct option wait_options[] = { + { "read-write", no_argument, NULL, 'W' }, + { "timeout", required_argument, NULL, 'T' }, { NULL, 0, NULL, 0 } }; - optind = 0; + optind = 1; + opterr = 0; + + int c; + int option_index = 0; - while ((c = getopt_long(argc, argv, "D:t:Vvqh", - long_options, &option_index)) != -1) + while ((c = getopt_long(argc, argv, "WT:", wait_options, &option_index)) != -1) { switch (c) { - case 'D': + case 'W': { - strlcpy(keeperOptions.pgSetup.pgdata, optarg, MAXPGPATH); - log_trace("--pgdata %s", optarg); + pgsetupWaitReadWrite = true; break; } - case 't': + case 'T': { - if (!stringToInt(optarg, &pgsetup_wait_timeout) || - pgsetup_wait_timeout <= 0) + int t = strtol(optarg, NULL, 10); + if (t <= 0) { - log_fatal( - "--timeout argument is not a valid positive integer: \"%s\"", - optarg); + log_error("--timeout must be a positive integer"); exit(EXIT_CODE_BAD_ARGS); } - log_trace("--timeout %d", pgsetup_wait_timeout); + pgsetupWaitTimeout = t; break; } - case 'V': + default: { - keeper_cli_print_version(argc, argv); + /* standard keeper options; handled by the delegated call below */ break; } + } + } - case 'v': - { - log_set_level(LOG_DEBUG); - break; - } + opterr = 1; - case 'q': - { - log_set_level(LOG_ERROR); - break; - } + /* + * Build a filtered argv that strips --timeout/--read-write (and their + * arguments) so that keeper_cli_keeper_setup_getopts does not see them. + */ + char **filtered_argv = (char **) palloc((argc + 1) * sizeof(char *)); + int filtered_argc = 0; - case 'h': - { - commandline_help(stderr); - exit(EXIT_CODE_QUIT); - break; - } + for (int i = 0; i < argc; i++) + { + if (strcmp(argv[i], "--read-write") == 0 || strcmp(argv[i], "-W") == 0) + { + continue; + } - default: - { - commandline_help(stderr); - exit(EXIT_CODE_BAD_ARGS); - break; - } + if ((strcmp(argv[i], "--timeout") == 0 || strcmp(argv[i], "-T") == 0) && + i + 1 < argc) + { + /* skip both the flag and its argument */ + i++; + continue; } + + filtered_argv[filtered_argc++] = argv[i]; } + filtered_argv[filtered_argc] = NULL; - /* publish parsed options */ - keeperOptions.pgSetup.pgdata[0] = - keeperOptions.pgSetup.pgdata[0]; /* no-op, already set above */ + int rc = keeper_cli_keeper_setup_getopts(filtered_argc, filtered_argv); - return optind; + pfree(filtered_argv); + + return rc; } /* - * keeper_cli_pgsetup_wait_until_ready waits until the local Postgres server - * is ready to accept connections, up to --timeout seconds (default 30). + * keeper_cli_pgsetup_wait_until_ready waits for the local Postgres server to + * become ready. When --read-write is given, it additionally waits until the + * server is accepting read-write connections (not in recovery and not set to + * default_transaction_read_only). + * + * The --timeout value (default 30s) is a single deadline shared by both + * phases: the pg_is_ready poll and the subsequent read-write connection + * attempt. Time spent waiting for Postgres to start counts against the + * budget for the read-write phase. */ void keeper_cli_pgsetup_wait_until_ready(int argc, char **argv) { + int timeout = pgsetupWaitTimeout; + ConfigFilePaths pathnames = { 0 }; LocalPostgresServer postgres = { 0 }; PostgresSetup *pgSetup = &(postgres.postgresSetup); + /* Record wall-clock start so all phases share one deadline. */ + time_t startTime = time(NULL); + + /* Wait up to `timeout` seconds for the config file to be created. + * In no-monitor mode, pg_autoctl create postgres runs first and writes the + * config; pgsetup wait may be called before that completes. */ + { + KeeperConfig kconfig = keeperOptions; + if (keeper_config_set_pathnames_from_pgdata(&(kconfig.pathnames), + kconfig.pgSetup.pgdata)) + { + time_t deadline = startTime + timeout; + while (!file_exists(kconfig.pathnames.config) && + time(NULL) < deadline) + { + log_debug("Waiting for config file \"%s\" to appear", + kconfig.pathnames.config); + pg_usleep(500 * 1000); + } + } + } + if (!cli_common_pgsetup_init(&pathnames, pgSetup)) { /* errors have already been logged */ @@ -433,16 +478,105 @@ keeper_cli_pgsetup_wait_until_ready(int argc, char **argv) log_debug("Initialized pgSetup, now calling pg_setup_wait_until_is_ready()"); + /* + * Phase 1: wait for postmaster to signal "ready" in postmaster.pid. + * Pass the remaining timeout so the two phases together stay within the + * single user-visible deadline. + */ + int remainingAfterConfig = timeout - (int) (time(NULL) - startTime); + if (remainingAfterConfig <= 0) + { + log_error("Timed out waiting for Postgres config file to appear"); + exit(EXIT_CODE_PGSQL); + } + bool pgIsReady = - pg_setup_wait_until_is_ready(pgSetup, pgsetup_wait_timeout, LOG_INFO); + pg_setup_wait_until_is_ready(pgSetup, remainingAfterConfig, LOG_INFO); log_info("Postgres status is: \"%s\"", pmStatusToString(pgSetup->pm_status)); - if (pgIsReady) + if (!pgIsReady) + { + exit(EXIT_CODE_PGSQL); + } + + if (!pgsetupWaitReadWrite) { + /* Plain "ready" check — we're done. */ exit(EXIT_CODE_QUIT); } - exit(EXIT_CODE_PGSQL); + + /* + * Phase 2: wait until the server accepts read-write connections. + * + * Postgres is up (phase 1 passed) but may still be in recovery, finishing + * pg_rewind, or in standby mode. We poll with a libpq connection that + * checks pg_is_in_recovery() until it returns false or the deadline fires. + * + * We use the local connection string from pgSetup (Unix socket when + * available, matching whatever auth the node was created with) so that the + * check works regardless of the cluster's auth method. + */ + char connstr[MAXCONNINFO]; + if (!pg_setup_get_local_connection_string(pgSetup, connstr)) + { + log_error("Failed to build local connection string for read-write check"); + exit(EXIT_CODE_BAD_CONFIG); + } + + log_info("Waiting for Postgres to accept read-write connections " + "(timeout %ds)", timeout); + + bool isReadWrite = false; + int attempts = 0; + + while (!isReadWrite) + { + int elapsed = (int) (time(NULL) - startTime); + int remaining = timeout - elapsed; + + if (remaining <= 0) + { + log_error("Timed out after %ds waiting for Postgres " + "to accept read-write connections", timeout); + exit(EXIT_CODE_PGSQL); + } + + /* Use a short per-attempt connect_timeout so we retry briskly. */ + char attemptConnstr[MAXCONNINFO]; + sformat(attemptConnstr, sizeof(attemptConnstr), + "%s connect_timeout=1", connstr); + + PGSQL pgsql = { 0 }; + pgsql_init(&pgsql, attemptConnstr, PGSQL_CONN_LOCAL); + + bool inRecovery = true; /* assume standby until proven otherwise */ + bool queryOk = pgsql_is_in_recovery(&pgsql, &inRecovery); + pgsql_finish(&pgsql); + + if (queryOk && !inRecovery) + { + isReadWrite = true; + break; + } + + /* let's not be THAT verbose about it */ + if (attempts % 10 == 0) + { + log_debug("pgsetup wait --read-write: attempt %d, " + "in_recovery=%s, after %ds", + attempts + 1, + inRecovery ? "true" : "false", + elapsed); + } + + ++attempts; + pg_usleep(100 * 1000); /* 100 ms between probes */ + } + + log_info("Postgres is now accepting read-write connections on port %d", + pgSetup->pgport); + exit(EXIT_CODE_QUIT); } diff --git a/src/bin/pg_autoctl/cli_do_root.c b/src/bin/pg_autoctl/cli_do_root.c index 9ee0752c7..59e3dd288 100644 --- a/src/bin/pg_autoctl/cli_do_root.c +++ b/src/bin/pg_autoctl/cli_do_root.c @@ -185,20 +185,13 @@ CommandLine do_pgsetup_is_ready = CommandLine do_pgsetup_wait_until_ready = make_command("wait", "Wait until the local Postgres server is ready", - "[option ...]", - " --pgdata path to data directory\n" - " --timeout seconds to wait, default 30\n", + "[--read-write] [--timeout N] [option ...]", + " --read-write also wait until the server accepts read-write connections\n" + " --timeout N total timeout in seconds (default: 30)\n" + KEEPER_CLI_WORKER_SETUP_OPTIONS, keeper_cli_pgsetup_wait_getopts, keeper_cli_pgsetup_wait_until_ready); -CommandLine do_pgsetup_hba_lan = - make_command("hba-lan", - "Add LAN CIDR trust rules to pg_hba.conf and reload", - "[option ...]", - KEEPER_CLI_WORKER_SETUP_OPTIONS, - keeper_cli_keeper_setup_getopts, - keeper_cli_pgsetup_hba_lan); - CommandLine do_pgsetup_startup_logs = make_command("logs", "Outputs the Postgres startup logs", @@ -215,6 +208,14 @@ CommandLine do_pgsetup_tune = keeper_cli_keeper_setup_getopts, keeper_cli_pgsetup_tune); +CommandLine do_pgsetup_hba_lan = + make_command("hba-lan", + "Append LAN CIDR trust rules to pg_hba.conf and reload Postgres", + "[option ...]", + KEEPER_CLI_WORKER_SETUP_OPTIONS, + keeper_cli_keeper_setup_getopts, + keeper_cli_pgsetup_hba_lan); + CommandLine *do_pgsetup[] = { &do_pgsetup_pg_ctl, &do_pgsetup_discover, @@ -385,53 +386,254 @@ CommandLine do_tmux_commands = NULL, NULL, NULL, do_tmux); /* - * internal service: hidden entry points spawned by the supervisor via - * fork+exec. The supervisor builds argv as: - * pg_autoctl internal service postgres|listener|node-active --pgdata ... - * Use make_hidden_command_set so these never appear in --help output. + * Azure integration has been removed. The commands that were here are no + * longer maintained and have been deleted from this file. See pgaftest for + * the replacement QA tooling. */ -static CommandLine *internal_service_subcommands[] = { - &service_pgcontroller, - &service_postgres, - &service_monitor_listener, - &service_node_active, + +#if 0 /* REMOVED: azure commands — see pgaftest instead */ +CommandLine do_azure_provision_region = + make_command("region", + "Provision an azure region: resource group, network, VMs", + "[option ...]", + " --prefix azure group name prefix (ha-demo)\n" + " --region name to use for referencing the region\n" + " --location azure location where to create a resource group\n" + " --monitor should we create a monitor in the region (false)\n" + " --nodes number of Postgres nodes to create (2)\n" + " --script output a shell script instead of creating resources\n", + cli_do_azure_getopts, + cli_do_azure_create_region); + +CommandLine do_azure_provision_nodes = + make_command("nodes", + "Provision our pre-created VM with pg_autoctl Postgres nodes", + "[option ...]", + " --prefix azure group name prefix (ha-demo)\n" + " --region name to use for referencing the region\n" + " --monitor should we create a monitor in the region (false)\n" + " --nodes number of Postgres nodes to create (2)\n" + " --script output a shell script instead of creating resources\n", + cli_do_azure_getopts, + cli_do_azure_create_nodes); + + +CommandLine *do_azure_provision[] = { + &do_azure_provision_region, + &do_azure_provision_nodes, NULL }; -CommandLine internal_service_commands = - make_hidden_command_set("service", - "Internal subprocess entry points (supervisor use only)", - NULL, NULL, NULL, internal_service_subcommands); +CommandLine do_azure_provision_commands = + make_command_set("provision", + "provision azure resources for a pg_auto_failover demo", + NULL, NULL, NULL, do_azure_provision); -static CommandLine *internal_subcommands[] = { - &internal_service_commands, +CommandLine do_azure_create = + make_command("create", + "Create an azure QA environment", + "[option ...]", + " --prefix azure group name prefix (ha-demo)\n" + " --region name to use for referencing the region\n" + " --location azure location to use for the resources\n" + " --nodes number of Postgres nodes to create (2)\n" + " --script output a script instead of creating resources\n" + " --no-monitor do not create the pg_autoctl monitor node\n" + " --no-app do not create the application node\n" + " --cidr use the 10.CIDR.CIDR.0/24 subnet (11)\n" + " --from-source provision pg_auto_failover from sources\n", + cli_do_azure_getopts, + cli_do_azure_create_environment); + +CommandLine do_azure_drop = + make_command("drop", + "Drop an azure QA environment: resource group, network, VMs", + "[option ...]", + " --prefix azure group name prefix (ha-demo)\n" + " --region name to use for referencing the region\n" + " --location azure location where to create a resource group\n" + " --monitor should we create a monitor in the region (false)\n" + " --nodes number of Postgres nodes to create (2)\n" + " --script output a shell script instead of creating resources\n", + cli_do_azure_getopts, + cli_do_azure_drop_region); + +CommandLine do_azure_deploy = + make_command("deploy", + "Deploy a pg_autoctl VMs, given by name", + "[option ...] vmName", + "", + cli_do_azure_getopts, + cli_do_azure_deploy); + +CommandLine do_azure_show_ips = + make_command("ips", + "Show public and private IP addresses for selected VMs", + "[option ...]", + " --prefix azure group name prefix (ha-demo)\n" + " --region name to use for referencing the region\n", + cli_do_azure_getopts, + cli_do_azure_show_ips); + +CommandLine do_azure_show_state = + make_command("state", + "Connect to the monitor node to show the current state", + "[option ...]", + " --prefix azure group name prefix (ha-demo)\n" + " --region name to use for referencing the region\n" + " --watch run the command again every 0.2s\n", + cli_do_azure_getopts, + cli_do_azure_show_state); + +CommandLine *do_azure_show[] = { + &do_azure_show_ips, + &do_azure_show_state, NULL }; -CommandLine internal_commands = - make_hidden_command_set("internal", - "Internal commands for use by the supervisor (not for operators)", - NULL, NULL, NULL, internal_subcommands); +CommandLine do_azure_show_commands = + make_command_set("show", + "show azure resources for a pg_auto_failover demo", + NULL, NULL, NULL, do_azure_show); +CommandLine do_azure_ls = + make_command("ls", + "List resources in a given azure region", + "[option ...]", + " --prefix azure group name prefix (ha-demo)\n" + " --region name to use for referencing the region\n", + cli_do_azure_getopts, + cli_do_azure_ls); + +CommandLine do_azure_ssh = + make_command("ssh", + "Runs ssh -l ha-admin for a given VM name", + "[option ...]", + " --prefix azure group name prefix (ha-demo)\n" + " --region name to use for referencing the region\n", + cli_do_azure_getopts, + cli_do_azure_ssh); + +CommandLine do_azure_sync = + make_command("sync", + "Rsync pg_auto_failover sources on all the target region VMs", + "[option ...]", + " --prefix azure group name prefix (ha-demo)\n" + " --region region to use for referencing the region\n" + " --monitor should we create a monitor in the region (false)\n" + " --nodes number of Postgres nodes to create (2)\n", + cli_do_azure_getopts, + cli_do_azure_rsync); + +CommandLine do_azure_tmux_session = + make_command("session", + "Create or attach a tmux session for the created Azure VMs", + "[option ...]", + " --prefix azure group name prefix (ha-demo)\n" + " --region region to use for referencing the region\n" + " --monitor should we create a monitor in the region (false)\n" + " --nodes number of Postgres nodes to create (2)\n", + cli_do_azure_getopts, + cli_do_azure_tmux_session); + +CommandLine do_azure_tmux_kill = + make_command("kill", + "Kill an existing tmux session for Azure VMs", + "[option ...]", + " --prefix azure group name prefix (ha-demo)\n" + " --region region to use for referencing the region\n" + " --monitor should we create a monitor in the region (false)\n" + " --nodes number of Postgres nodes to create (2)\n", + cli_do_azure_getopts, + cli_do_azure_tmux_kill); + +CommandLine *do_azure_tmux[] = { + &do_azure_tmux_session, + &do_azure_tmux_kill, + NULL +}; + +CommandLine do_azure_tmux_commands = + make_command_set("tmux", + "Run a tmux session with an Azure setup for QA/testing", + NULL, NULL, NULL, do_azure_tmux); + +CommandLine *do_azure[] = { + &do_azure_provision_commands, + &do_azure_tmux_commands, + &do_azure_show_commands, + &do_azure_deploy, + &do_azure_create, + &do_azure_drop, + &do_azure_ls, + &do_azure_ssh, + &do_azure_sync, + NULL +}; + +CommandLine do_azure_commands = + make_command_set("azure", + "Manage a set of Azure resources for a pg_auto_failover demo", + NULL, NULL, NULL, do_azure); + +#endif /* REMOVED: azure commands */ + +/* + * pg_autoctl internal service postgres|listener|node-active + * + * These are the subprocess entry points used by the supervisor (pg_autoctl run + * and pg_autoctl create … --run). The supervisor fork()s and then execv()s + * the pg_autoctl binary itself with one of these sub-commands so that each + * service runs in its own address space. + * + * Using fork+exec (rather than fork alone) is a deliberate design choice for + * live upgrades: when a child process exits with an incompatible monitor + * extension version, the supervisor restarts it via fork()+execv(), which loads + * the current binary from disk. If the binary has been updated in place (e.g. + * by a package manager), the restarted child automatically picks up the new + * version without touching the supervisor process — making pg_autoctl safe to + * use as PID 1 in Docker/Kubernetes containers where replacing the binary and + * sending SIGTERM would lose the container. + * + * See also: keeper.c keeper_check_monitor_extension_version(), which exits on + * version mismatch precisely to trigger this restart-with-new-binary path. + * + * These commands are hidden from --help output (make_hidden_command_set) so + * operators do not accidentally invoke them directly. + * Use "pg_autoctl manual service" for the user-facing controls (restart, pgctl). + * Use "pg_autoctl inspect getpid" to read sub-process PIDs. + */ +static CommandLine *internal_service_subcommands[] = { + &service_pgcontroller, /* debug: supervisor for just the postgres controller */ + &service_postgres, /* spawned by service_postgres_ctl_start() */ + &service_monitor_listener, /* spawned by service_monitor_start() */ + &service_node_active, /* spawned by service_keeper_start() */ + NULL +}; + +static CommandLine internal_service_commands = + make_hidden_command_set("service", + "Subprocess entry points for the pg_autoctl supervisor", + NULL, NULL, NULL, internal_service_subcommands); + +/* + * pg_autoctl internal + * + * Hidden from --help; routable so the supervisor's execv() calls work. + * Contains only what the supervisor spawns plus dev/QA tooling not yet + * moved to pgaftest (tmux, demo). + */ CommandLine *do_subcommands[] = { - &do_monitor_commands, - &do_coordinator_commands, - &do_fsm_commands, - &do_primary_, - &do_standby_, - &do_show_commands, - &do_pgsetup_commands, - &do_service_postgres_ctl_commands, - &do_service_commands, + &internal_service_commands, &do_tmux_commands, &do_demo_commands, NULL }; CommandLine do_commands = - make_command_set("do", - "Internal commands and internal QA tooling", NULL, NULL, - NULL, do_subcommands); + make_hidden_command_set("internal", + "Internal subprocess entry points — not for direct use", + NULL, NULL, NULL, do_subcommands); /* diff --git a/src/bin/pg_autoctl/cli_do_root.h b/src/bin/pg_autoctl/cli_do_root.h index 888f68f9f..1c3112dd6 100644 --- a/src/bin/pg_autoctl/cli_do_root.h +++ b/src/bin/pg_autoctl/cli_do_root.h @@ -1,7 +1,7 @@ /* * src/bin/pg_autoctl/cli_do_root.h - * Implementation of a CLI which lets you run operations on the local - * postgres server directly + * Implementation of a CLI which lets you run individual keeper routines + * directly * * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the PostgreSQL License. @@ -15,33 +15,40 @@ /* src/bin/pg_autoctl/cli_do_fsm.c */ extern CommandLine do_fsm_commands; -extern CommandLine fsm_nodes; -/* Exported individually so cli_inspect.c and cli_manual.c can compose them */ -extern CommandLine fsm_init; +/* read-only sub-commands exposed via "pg_autoctl inspect fsm" */ extern CommandLine fsm_state; +extern CommandLine fsm_node_state; extern CommandLine fsm_list; extern CommandLine fsm_gv; + +/* mutating sub-commands exposed via "pg_autoctl manual fsm" */ +extern CommandLine fsm_init; extern CommandLine fsm_assign; extern CommandLine fsm_step; +extern CommandLine fsm_nodes; /* nodes get + nodes set — kept together in manual */ /* src/bin/pg_autoctl/cli_do_monitor.c */ extern CommandLine do_monitor_commands; -/* Exported individually so cli_inspect.c and cli_manual.c can compose them */ +/* read-only sub-commands exposed via "pg_autoctl inspect monitor" */ extern CommandLine monitor_get_command; extern CommandLine monitor_parse_notification_command; +extern CommandLine monitor_node_state_command; +extern CommandLine monitor_formation_states_command; + +/* mutating sub-commands exposed via "pg_autoctl manual monitor" */ extern CommandLine monitor_register_command; extern CommandLine monitor_node_active_command; extern CommandLine monitor_version_command; /* src/bin/pg_autoctl/cli_do_service.c */ extern CommandLine do_service_commands; -extern CommandLine do_service_restart_commands; extern CommandLine do_service_getpid_commands; +extern CommandLine do_service_restart_commands; extern CommandLine do_service_postgres_ctl_commands; -/* Internal subprocess entry points used in cli_do_root.c's internal_service_commands */ +/* subprocess entry points (spawned by the supervisor via fork+exec) */ extern CommandLine service_pgcontroller; extern CommandLine service_postgres; extern CommandLine service_monitor_listener; @@ -50,24 +57,32 @@ extern CommandLine service_node_active; /* src/bin/pg_autoctl/cli_do_show.c */ extern CommandLine do_show_commands; extern CommandLine do_pgsetup_commands; +extern CommandLine do_service_postgres_ctl_commands; +extern CommandLine do_service_commands; /* src/bin/pg_autoctl/cli_do_demo.c */ extern CommandLine do_demo_commands; -/* src/bin/pg_autoctl/cli_do_coordinator.c */ -extern CommandLine do_coordinator_commands; - /* src/bin/pg_autoctl/cli_do_root.c */ extern CommandLine do_primary_adduser; extern CommandLine *do_primary_adduser_subcommands[]; extern CommandLine do_primary_adduser_monitor; extern CommandLine do_primary_adduser_replica; +extern CommandLine do_primary_syncrep_; +extern CommandLine *do_primary_syncrep[]; +extern CommandLine do_primary_syncrep_enable; +extern CommandLine do_primary_syncrep_disable; + extern CommandLine do_primary_slot_; extern CommandLine *do_primary_slot[]; extern CommandLine do_primary_slot_create; extern CommandLine do_primary_slot_drop; +extern CommandLine do_primary_hba; +extern CommandLine *do_primary_hba_commands[]; +extern CommandLine do_primary_hba_setup; + extern CommandLine do_primary_defaults; extern CommandLine do_primary_identify_system; @@ -80,10 +95,12 @@ extern CommandLine do_standby_init; extern CommandLine do_standby_rewind; extern CommandLine do_standby_promote; +extern CommandLine do_discover; + extern CommandLine do_tmux_commands; -extern CommandLine internal_service_commands; -extern CommandLine internal_commands; +/* src/bin/pg_autoctl/cli_do_coordinator.c */ +extern CommandLine do_coordinator_commands; extern CommandLine do_commands; extern CommandLine *do_subcommands[]; @@ -98,13 +115,13 @@ void keeper_cli_enable_synchronous_replication(int argc, char **argv); void keeper_cli_disable_synchronous_replication(int argc, char **argv); void keeper_cli_pgsetup_pg_ctl(int argc, char **argv); -void keeper_cli_pgsetup_hba_lan(int argc, char **argv); void keeper_cli_pgsetup_discover(int argc, char **argv); void keeper_cli_pgsetup_is_ready(int argc, char **argv); int keeper_cli_pgsetup_wait_getopts(int argc, char **argv); void keeper_cli_pgsetup_wait_until_ready(int argc, char **argv); void keeper_cli_pgsetup_startup_logs(int argc, char **argv); void keeper_cli_pgsetup_tune(int argc, char **argv); +void keeper_cli_pgsetup_hba_lan(int argc, char **argv); void keeper_cli_add_default_settings(int argc, char **argv); void keeper_cli_create_monitor_user(int argc, char **argv); @@ -130,4 +147,6 @@ void cli_do_tmux_compose_config(int argc, char **argv); void cli_do_tmux_compose_script(int argc, char **argv); void cli_do_tmux_compose_session(int argc, char **argv); +/* Azure integration removed — use pgaftest instead */ + #endif /* CLI_DO_ROOT_H */ diff --git a/src/bin/pg_autoctl/cli_drop_node.c b/src/bin/pg_autoctl/cli_drop_node.c index a16e52a6f..04a1bf0c1 100644 --- a/src/bin/pg_autoctl/cli_drop_node.c +++ b/src/bin/pg_autoctl/cli_drop_node.c @@ -48,6 +48,7 @@ */ bool dropAndDestroy = false; static bool dropForce = false; +static bool dropNoWait = false; static void cli_drop_monitor(int argc, char **argv); @@ -84,8 +85,7 @@ CommandLine drop_node_command = " --pgport drop the node with given hostname and pgport\n" " --destroy also destroy Postgres database\n" " --force force dropping the node from the monitor\n" - " --wait how many seconds to wait, default to 60\n" - " --no-wait drop the node without waiting for confirmation\n", + " --wait how many seconds to wait, default to 60 \n", cli_drop_node_getopts, cli_drop_node); @@ -164,8 +164,7 @@ cli_drop_node_getopts(int argc, char **argv) case 'W': { - /* --no-wait: set timeout to zero so the notification loop is skipped */ - options.listen_notifications_timeout = 0; + dropNoWait = true; log_trace("--no-wait"); break; } @@ -605,6 +604,19 @@ cli_drop_local_node(KeeperConfig *config, bool dropAndDestroy) (void) cli_drop_node_from_monitor(config, &nodeId, &groupId); } + /* + * With --no-wait the caller takes responsibility for waiting until the + * supervisor has stopped (e.g. via `docker compose wait` in a container + * environment). The running keeper will detect it has been dropped on its + * next node_active() heartbeat and exit cleanly on its own. + */ + if (dropNoWait) + { + log_info("Node unregistered from monitor; not waiting for the local " + "pg_autoctl process to stop (--no-wait)."); + exit(EXIT_CODE_QUIT); + } + /* * Now, when the pg_autoctl keeper service is still running, wait until * it has reached the DROPPED/DROPPED state on-disk and then exited. diff --git a/src/bin/pg_autoctl/keeper_config.c b/src/bin/pg_autoctl/keeper_config.c index 1459336df..553819ee3 100644 --- a/src/bin/pg_autoctl/keeper_config.c +++ b/src/bin/pg_autoctl/keeper_config.c @@ -133,6 +133,11 @@ config->replication_password, \ REPLICATION_PASSWORD_DEFAULT) +#define OPTION_AUTOCTL_MONITOR_PASSWORD(config) \ + make_strbuf_option_default("pg_autoctl", "monitor_password", NULL, \ + false, MAXCONNINFO, \ + config->monitor_password, "") + #define OPTION_REPLICATION_MAXIMUM_BACKUP_RATE(config) \ make_strbuf_option_default("replication", "maximum_backup_rate", NULL, \ false, MAXIMUM_BACKUP_RATE_LEN, \ @@ -241,6 +246,7 @@ OPTION_REPLICATION_MAXIMUM_BACKUP_RATE(config), \ OPTION_REPLICATION_BACKUP_DIR(config), \ OPTION_REPLICATION_PASSWORD(config), \ + OPTION_AUTOCTL_MONITOR_PASSWORD(config), \ OPTION_TIMEOUT_NETWORK_PARTITION(config), \ OPTION_TIMEOUT_PREPARE_PROMOTION_CATCHUP(config), \ OPTION_TIMEOUT_PREPARE_PROMOTION_WALRECEIVER(config), \ diff --git a/src/bin/pg_autoctl/keeper_config.h b/src/bin/pg_autoctl/keeper_config.h index 3826923d9..73e6cbd62 100644 --- a/src/bin/pg_autoctl/keeper_config.h +++ b/src/bin/pg_autoctl/keeper_config.h @@ -53,6 +53,7 @@ typedef struct KeeperConfig /* PostgreSQL replication / tooling setup */ char replication_slot_name[MAXCONNINFO]; char replication_password[MAXCONNINFO]; + char monitor_password[MAXCONNINFO]; char maximum_backup_rate[MAXIMUM_BACKUP_RATE_LEN]; char backupDirectory[MAXPGPATH]; diff --git a/src/bin/pg_autoctl/monitor_config.c b/src/bin/pg_autoctl/monitor_config.c index f62be0d8b..ce88349b8 100644 --- a/src/bin/pg_autoctl/monitor_config.c +++ b/src/bin/pg_autoctl/monitor_config.c @@ -36,6 +36,11 @@ make_strbuf_option("pg_autoctl", "hostname", "hostname", \ false, _POSIX_HOST_NAME_MAX, config->hostname) +#define OPTION_AUTOCTL_NODE_PASSWORD(config) \ + make_strbuf_option_default("pg_autoctl", "autoctl_node_password", NULL, \ + false, MAXCONNINFO, \ + config->autoctl_node_password, "") + #define OPTION_AUTOCTL_NODENAME(config) \ make_strbuf_compat_option("pg_autoctl", "nodename", \ _POSIX_HOST_NAME_MAX, config->hostname) @@ -104,6 +109,7 @@ OPTION_AUTOCTL_ROLE(config), \ OPTION_AUTOCTL_HOSTNAME(config), \ OPTION_AUTOCTL_NODENAME(config), \ + OPTION_AUTOCTL_NODE_PASSWORD(config), \ OPTION_POSTGRESQL_PGDATA(config), \ OPTION_POSTGRESQL_PG_CTL(config), \ OPTION_POSTGRESQL_USERNAME(config), \ @@ -118,9 +124,6 @@ OPTION_SSL_CRL_FILE(config), \ OPTION_SSL_SERVER_CERT(config), \ OPTION_SSL_SERVER_KEY(config), \ - make_strbuf_option_default("pg_auto_failover", "autoctl_node_password", \ - NULL, false, MAXCONNINFO, \ - config->autoctl_node_password, ""), \ INI_OPTION_LAST \ } diff --git a/src/bin/pg_autoctl/monitor_config.h b/src/bin/pg_autoctl/monitor_config.h index b30a8ff8d..97c6eee54 100644 --- a/src/bin/pg_autoctl/monitor_config.h +++ b/src/bin/pg_autoctl/monitor_config.h @@ -25,6 +25,7 @@ typedef struct MonitorConfig /* pg_autoctl setup */ char hostname[_POSIX_HOST_NAME_MAX]; + char autoctl_node_password[MAXCONNINFO]; /* PostgreSQL setup */ char role[NAMEDATALEN]; @@ -32,8 +33,11 @@ typedef struct MonitorConfig /* PostgreSQL setup */ PostgresSetup pgSetup; - /* password for the autoctl_node role; "" means no password (trust) */ - char autoctl_node_password[MAXCONNINFO]; + /* non-default formations to create during monitor init */ +#define MONITOR_MAX_FORMATIONS 16 + int formationCount; + char formationNames[MONITOR_MAX_FORMATIONS][NAMEDATALEN]; + char formationKinds[MONITOR_MAX_FORMATIONS][NAMEDATALEN]; /* "pgsql" = default */ } MonitorConfig; diff --git a/src/bin/pg_autoctl/monitor_pg_init.c b/src/bin/pg_autoctl/monitor_pg_init.c index 6f99e2e8c..839cd5111 100644 --- a/src/bin/pg_autoctl/monitor_pg_init.c +++ b/src/bin/pg_autoctl/monitor_pg_init.c @@ -217,6 +217,23 @@ monitor_install(const char *hostname, return false; } + /* + * If a password for the autoctl_node role has been configured, set it now. + * The autoctl_node role is created by the pgautofailover extension (via + * CREATE EXTENSION above), so we ALTER it here after the extension exists. + */ + if (autoctl_node_password != NULL && autoctl_node_password[0] != '\0') + { + if (!pgsql_alter_role_password(&postgres.sqlClient, + PG_AUTOCTL_MONITOR_USERNAME, + autoctl_node_password)) + { + log_error("Failed to set password for role \"%s\"", + PG_AUTOCTL_MONITOR_USERNAME); + return false; + } + } + /* * When installing the monitor on-top of an already running PostgreSQL, we * want to check that our settings have been applied already, and warn the @@ -250,18 +267,6 @@ monitor_install(const char *hostname, return false; } - if (autoctl_node_password != NULL && autoctl_node_password[0] != '\0') - { - if (!pgsql_alter_role_password(&postgres.sqlClient, - PG_AUTOCTL_MONITOR_USERNAME, - autoctl_node_password)) - { - log_error("Failed to set password for role \"%s\"", - PG_AUTOCTL_MONITOR_USERNAME); - return false; - } - } - log_info("Your pg_auto_failover monitor instance is now ready on port %d.", pgSetup.pgport); diff --git a/src/bin/pg_autoctl/nodespec.c b/src/bin/pg_autoctl/nodespec.c index 5f61e6d91..54d142c82 100644 --- a/src/bin/pg_autoctl/nodespec.c +++ b/src/bin/pg_autoctl/nodespec.c @@ -114,8 +114,7 @@ nodespec_read(const char *path, NodeSpec *spec) sizeof(replicationQuorumStr), replicationQuorumStr, "true"), - /* [options] — ssl is mutable (applied via `pg_autoctl enable ssl`); - * auth and pg_hba_lan are create-time only */ + /* [options] — immutable, used only at create time */ make_strbuf_option_default("options", "ssl", NULL, false, sizeof(spec->ssl), spec->ssl, "self-signed"), @@ -684,19 +683,15 @@ nodespec_write_to_path(const NodeSpec *spec, const char *path) * nodespec_apply compares new_spec against old_spec and applies any changes * to the mutable fields by calling into the keeper / monitor APIs. * - * Mutable fields: - * - candidate_priority → pg_autoctl set node candidate-priority - * - replication_quorum → pg_autoctl set node replication-quorum - * - ssl / ssl_*_file → pg_autoctl enable ssl - * - monitor_pguri → pg_autoctl disable monitor --force - * pg_autoctl enable monitor - * - * Immutable fields (kind, pgdata, hostname, port, auth, - * pg_hba_lan) require a node restart to take effect. + * Currently mutable fields: + * - candidate_priority → monitor_set_node_candidate_priority() + * - replication_quorum → monitor_set_node_replication_quorum() * * The [launch] mode field is handled separately by pg_autoctl node start. * Applying a spec with mode=deferred to an already-started node is a * non-fatal warning (ignored). + * + * Immutable fields (kind, pgdata, ssl, auth, pg_hba_lan) are not checked here. */ bool nodespec_apply(const NodeSpec *new_spec, const NodeSpec *old_spec) @@ -757,122 +752,6 @@ nodespec_apply(const NodeSpec *new_spec, const NodeSpec *old_spec) free_program(&prog); } - /* - * Monitor URI changed: disable the current monitor (removing this node - * from it) then re-register to the new one. The --force flag allows - * disable to proceed even if the old monitor is unreachable. - */ - if (strcmp(new_spec->monitor_pguri, old_spec->monitor_pguri) != 0 && - !IS_EMPTY_STRING_BUFFER(new_spec->monitor_pguri)) - { - Program disable_prog = run_program(pg_autoctl_program, - "disable", "monitor", - "--force", - "--pgdata", new_spec->pgdata, - NULL); - - if (disable_prog.returnCode != 0) - { - log_warn("nodespec_apply: disable monitor failed (rc=%d)", - disable_prog.returnCode); - if (disable_prog.stdOut) - { - log_warn("%s", disable_prog.stdOut); - } - free_program(&disable_prog); - } - else - { - free_program(&disable_prog); - - Program enable_prog = run_program(pg_autoctl_program, - "enable", "monitor", - "--pgdata", new_spec->pgdata, - new_spec->monitor_pguri, - NULL); - - if (enable_prog.returnCode != 0) - { - log_warn("nodespec_apply: enable monitor \"%s\" failed (rc=%d)", - new_spec->monitor_pguri, enable_prog.returnCode); - if (enable_prog.stdOut) - { - log_warn("%s", enable_prog.stdOut); - } - } - else - { - log_info("nodespec: applied monitor_pguri = %s", - new_spec->monitor_pguri); - changed = true; - } - free_program(&enable_prog); - } - } - - /* - * SSL mode or certificate paths changed: call `pg_autoctl enable ssl` - * with the appropriate flags so Postgres is reconfigured live. - * - * ssl=off maps to --no-ssl; ssl=self-signed maps to --ssl-self-signed; - * anything else is a CA-verified mode and requires the cert paths. - */ - { - bool ssl_changed = - (strcmp(new_spec->ssl, old_spec->ssl) != 0) || - (strcmp(new_spec->ssl_ca_file, old_spec->ssl_ca_file) != 0) || - (strcmp(new_spec->ssl_cert_file, old_spec->ssl_cert_file) != 0) || - (strcmp(new_spec->ssl_key_file, old_spec->ssl_key_file) != 0); - - if (ssl_changed) - { - Program prog; - - if (strcmp(new_spec->ssl, "off") == 0) - { - prog = run_program(pg_autoctl_program, - "enable", "ssl", - "--pgdata", new_spec->pgdata, - "--no-ssl", NULL); - } - else if (strcmp(new_spec->ssl, "self-signed") == 0 || - IS_EMPTY_STRING_BUFFER(new_spec->ssl_ca_file)) - { - prog = run_program(pg_autoctl_program, - "enable", "ssl", - "--pgdata", new_spec->pgdata, - "--ssl-self-signed", NULL); - } - else - { - prog = run_program(pg_autoctl_program, - "enable", "ssl", - "--pgdata", new_spec->pgdata, - "--ssl-mode", new_spec->ssl, - "--ssl-ca-file", new_spec->ssl_ca_file, - "--server-cert", new_spec->ssl_cert_file, - "--server-key", new_spec->ssl_key_file, - NULL); - } - - if (prog.returnCode != 0) - { - log_warn("nodespec_apply: enable ssl failed (rc=%d)", - prog.returnCode); - if (prog.stdOut) - { - log_warn("%s", prog.stdOut); - } - } - else - { - log_info("nodespec: applied ssl = %s", new_spec->ssl); - changed = true; - } - free_program(&prog); - } - } - /* immediate → deferred on an already-started node: non-fatal, ignored */ if (!old_spec->launchDeferred && new_spec->launchDeferred) { diff --git a/src/bin/pg_autoctl/nodespec.h b/src/bin/pg_autoctl/nodespec.h index 128ef15fb..3328643f1 100644 --- a/src/bin/pg_autoctl/nodespec.h +++ b/src/bin/pg_autoctl/nodespec.h @@ -59,9 +59,7 @@ typedef struct NodeSpec /* [postgresql] */ char pgdata[MAXPGPATH]; - /* [monitor] — empty for kind == monitor - * monitor_pguri is mutable: changing it triggers disable monitor --force - * followed by enable monitor (re-registers without restarting). */ + /* [monitor] — empty for kind == monitor */ char monitor_pguri[MAXCONNINFO]; bool noMonitor; /* [monitor] no_monitor=true: standalone mode */ int nodeId; /* [monitor] node_id: required with --disable-monitor */ diff --git a/src/bin/pg_autoctl/service_monitor_init.c b/src/bin/pg_autoctl/service_monitor_init.c index 583e09b29..02d8c9c9e 100644 --- a/src/bin/pg_autoctl/service_monitor_init.c +++ b/src/bin/pg_autoctl/service_monitor_init.c @@ -143,6 +143,27 @@ service_monitor_init_start(void *context, pid_t *pid) log_info("Monitor has been successfully initialized."); + /* create any non-default formations requested via --formation */ + for (int fi = 0; fi < config->formationCount; fi++) + { + char *fname = config->formationNames[fi]; + char *fkind = config->formationKinds[fi][0] + ? config->formationKinds[fi] : "pgsql"; + + log_info("Creating formation \"%s\" (kind %s)", fname, fkind); + + if (!monitor_create_formation(monitor, fname, fkind, + DEFAULT_DATABASE_NAME, + + /* hasSecondary */ true, + + /* numberSyncStandbys */ 0)) + { + log_error("Failed to create formation \"%s\"", fname); + exit(EXIT_CODE_INTERNAL_ERROR); + } + } + if (createAndRun) { /* here we call execv() so we never get back */ diff --git a/tests/tap/schedules/monitor.sch b/tests/tap/schedules/monitor.sch new file mode 100644 index 000000000..44ff1cca7 --- /dev/null +++ b/tests/tap/schedules/monitor.sch @@ -0,0 +1,4 @@ +# Monitor operations: disabled, replace, extension update (~15 min) +monitor_disabled +replace_monitor +extension_update diff --git a/tests/tap/schedules/multi-1.sch b/tests/tap/schedules/multi-1.sch new file mode 100644 index 000000000..6529cbd4d --- /dev/null +++ b/tests/tap/schedules/multi-1.sch @@ -0,0 +1,4 @@ +# Multi-node HA: standbys, maintenance, async replication (~18 min) +multi_standbys +multi_maintenance +multi_async diff --git a/tests/tap/schedules/multi-2.sch b/tests/tap/schedules/multi-2.sch new file mode 100644 index 000000000..685a7cf8b --- /dev/null +++ b/tests/tap/schedules/multi-2.sch @@ -0,0 +1,4 @@ +# Multi-node failover: ensure, alternate failover, network partition (~20 min) +ensure +multi_alternate +multi_ifdown diff --git a/tests/tap/schedules/node.sch b/tests/tap/schedules/node.sch new file mode 100644 index 000000000..be0d2ffe2 --- /dev/null +++ b/tests/tap/schedules/node.sch @@ -0,0 +1,4 @@ +# Node lifecycle: create, drop, auth (~10 min) +create_standby_with_pgdata +maintenance_and_drop +auth diff --git a/tests/tap/schedules/quick.sch b/tests/tap/schedules/quick.sch new file mode 100644 index 000000000..c9d6e7529 --- /dev/null +++ b/tests/tap/schedules/quick.sch @@ -0,0 +1,5 @@ +# Fast single-node and config tests (~10 min) +basic_operation +basic_operation_listen_flag +config_get_set +skip_pg_hba diff --git a/tests/tap/schedules/ssl.sch b/tests/tap/schedules/ssl.sch new file mode 100644 index 000000000..8a8ec2f1b --- /dev/null +++ b/tests/tap/schedules/ssl.sch @@ -0,0 +1,4 @@ +# SSL: self-signed, enable, cert auth (~20 min; ssl_cert is slowest) +enable_ssl +ssl_self_signed +ssl_cert From 61c077aa11632f8db3845d01924ff4bb313b6dd4 Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Tue, 7 Jul 2026 16:04:51 +0200 Subject: [PATCH 34/68] fix: resolve undeclared root_with_debug and internal_commands symbols MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - keeper_cli_help() referenced root_with_debug which was removed as part of the inspect/manual refactor (root_with_debug was only needed when do commands were hidden behind PG_AUTOCTL_DEBUG). Replace with a direct call using root, which already includes inspect/manual/internal. - cli_root.c root_subcommands referenced &internal_commands and &do_commands as two separate entries, but cli_do_root.c only defined do_commands. Rename do_commands → internal_commands in cli_do_root.c/h and drop the duplicate &do_commands slot from root_subcommands. --- src/bin/pg_autoctl/cli_common.c | 9 +-------- src/bin/pg_autoctl/cli_do_root.c | 2 +- src/bin/pg_autoctl/cli_do_root.h | 2 +- src/bin/pg_autoctl/cli_root.c | 2 -- 4 files changed, 3 insertions(+), 12 deletions(-) diff --git a/src/bin/pg_autoctl/cli_common.c b/src/bin/pg_autoctl/cli_common.c index 898b7bf5c..08416e0c6 100644 --- a/src/bin/pg_autoctl/cli_common.c +++ b/src/bin/pg_autoctl/cli_common.c @@ -1438,14 +1438,7 @@ exit_unless_role_is_keeper(KeeperConfig *kconfig) void keeper_cli_help(int argc, char **argv) { - CommandLine command = root; - - if (env_exists(PG_AUTOCTL_DEBUG)) - { - command = root_with_debug; - } - - (void) commandline_print_command_tree(&command, stdout); + (void) commandline_print_command_tree(&root, stdout); } diff --git a/src/bin/pg_autoctl/cli_do_root.c b/src/bin/pg_autoctl/cli_do_root.c index 59e3dd288..665d69521 100644 --- a/src/bin/pg_autoctl/cli_do_root.c +++ b/src/bin/pg_autoctl/cli_do_root.c @@ -630,7 +630,7 @@ CommandLine *do_subcommands[] = { NULL }; -CommandLine do_commands = +CommandLine internal_commands = make_hidden_command_set("internal", "Internal subprocess entry points — not for direct use", NULL, NULL, NULL, do_subcommands); diff --git a/src/bin/pg_autoctl/cli_do_root.h b/src/bin/pg_autoctl/cli_do_root.h index 1c3112dd6..ab6f5aa22 100644 --- a/src/bin/pg_autoctl/cli_do_root.h +++ b/src/bin/pg_autoctl/cli_do_root.h @@ -102,7 +102,7 @@ extern CommandLine do_tmux_commands; /* src/bin/pg_autoctl/cli_do_coordinator.c */ extern CommandLine do_coordinator_commands; -extern CommandLine do_commands; +extern CommandLine internal_commands; extern CommandLine *do_subcommands[]; int keeper_cli_keeper_setup_getopts(int argc, char **argv); diff --git a/src/bin/pg_autoctl/cli_root.c b/src/bin/pg_autoctl/cli_root.c index 3c9221109..df0736974 100644 --- a/src/bin/pg_autoctl/cli_root.c +++ b/src/bin/pg_autoctl/cli_root.c @@ -104,8 +104,6 @@ CommandLine *root_subcommands[] = { &manual_commands, &internal_commands, &node_commands, - - &do_commands, &service_run_command, &watch_command, &service_stop_command, From cdcb4d8cc61292e9889240c746cb9117408d0306 Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Tue, 7 Jul 2026 16:17:02 +0200 Subject: [PATCH 35/68] tests: add node-extra, citus-1, citus-2 schedules; include all specs in CI - node-extra.sch: debian_clusters + tablespaces - citus-1.sch: citus_cluster_name, citus_force_failover, citus_skip_pg_hba - citus-2.sch: basic_citus_operation, nonha_citus_operation, citus_multi_standbys - run-pgaftest.yml: expand matrix from 6 to 9 schedules (36 jobs total) All 27 specs now covered: 9 schedules + installcheck + upgrade dedicated jobs. --- .github/workflows/run-pgaftest.yml | 3 +++ tests/tap/schedules/citus-1.sch | 4 ++++ tests/tap/schedules/citus-2.sch | 4 ++++ tests/tap/schedules/node-extra.sch | 3 +++ 4 files changed, 14 insertions(+) create mode 100644 tests/tap/schedules/citus-1.sch create mode 100644 tests/tap/schedules/citus-2.sch create mode 100644 tests/tap/schedules/node-extra.sch diff --git a/.github/workflows/run-pgaftest.yml b/.github/workflows/run-pgaftest.yml index 938d5a2f2..1031bae9a 100644 --- a/.github/workflows/run-pgaftest.yml +++ b/.github/workflows/run-pgaftest.yml @@ -119,10 +119,13 @@ jobs: schedule: - quick # basic_operation, basic_operation_listen_flag, config_get_set, skip_pg_hba - node # create_standby_with_pgdata, maintenance_and_drop, auth + - node-extra # debian_clusters, tablespaces - monitor # monitor_disabled, replace_monitor, extension_update - ssl # enable_ssl, ssl_self_signed, ssl_cert - multi-1 # multi_standbys, multi_maintenance, multi_async - multi-2 # ensure, multi_alternate, multi_ifdown + - citus-1 # citus_cluster_name, citus_force_failover, citus_skip_pg_hba + - citus-2 # basic_citus_operation, nonha_citus_operation, citus_multi_standbys steps: - uses: actions/checkout@v7.0.0 diff --git a/tests/tap/schedules/citus-1.sch b/tests/tap/schedules/citus-1.sch new file mode 100644 index 000000000..b25f99030 --- /dev/null +++ b/tests/tap/schedules/citus-1.sch @@ -0,0 +1,4 @@ +# Citus basic tests: cluster name, forced failover, skip-pg-hba (~15 min) +citus_cluster_name +citus_force_failover +citus_skip_pg_hba diff --git a/tests/tap/schedules/citus-2.sch b/tests/tap/schedules/citus-2.sch new file mode 100644 index 000000000..58436119d --- /dev/null +++ b/tests/tap/schedules/citus-2.sch @@ -0,0 +1,4 @@ +# Citus advanced tests: multi-standby and full HA operation (~20 min) +basic_citus_operation +nonha_citus_operation +citus_multi_standbys diff --git a/tests/tap/schedules/node-extra.sch b/tests/tap/schedules/node-extra.sch new file mode 100644 index 000000000..a1b45c542 --- /dev/null +++ b/tests/tap/schedules/node-extra.sch @@ -0,0 +1,3 @@ +# Debian-style split-config layout and tablespace HA (~15 min) +debian_clusters +tablespaces From f74fe761d60a2ab6291e0e7d3e684e1928b1af4d Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Tue, 7 Jul 2026 16:43:12 +0200 Subject: [PATCH 36/68] tests: fix CI flakes in basic_operation, auth, and enable_ssl specs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit basic_operation.pgaf: - test_015_fail_secondary: wait until node1 stopped 30s → 60s (compose stop can be slow on shared CI runners) - test_016_drop_secondary: same wait until node1 stopped 30s → 60s - test_022_detect_network_partition: both waits 90s → 150s (demote_timeout transition involves monitor health-check cycles; shared CI runners can be slow to process the network partition signal) auth.pgaf: - setup: wait until primary, secondary 120s → 180s (cert-auth cluster formation is slower on shared CI runners) enable_ssl.pgaf: - test_007_enable_ssl_secondary: remove pgsetup wait after compose stop/start. node2 is still in maintenance mode when the container restarts — the monitor-assigned state is 'maintenance' so pg_autoctl does not start Postgres. The original Python test also does not assert Postgres is running at this point (the assert is in test_008). - test_008_disable_maintenance: add pgsetup wait --timeout 90 after disabling maintenance, so we confirm Postgres is up with SSL before the replication checks in test_009. --- tests/tap/specs/auth.pgaf | 3 ++- tests/tap/specs/basic_operation.pgaf | 12 ++++++++---- tests/tap/specs/enable_ssl.pgaf | 9 ++++++--- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/tests/tap/specs/auth.pgaf b/tests/tap/specs/auth.pgaf index 4301c2492..ab7e5fa4b 100644 --- a/tests/tap/specs/auth.pgaf +++ b/tests/tap/specs/auth.pgaf @@ -31,7 +31,8 @@ cluster { } setup { - wait until primary, secondary timeout 120s + # 180s: cert-auth cluster formation is slower on shared CI runners + wait until primary, secondary timeout 180s promote node1 } diff --git a/tests/tap/specs/basic_operation.pgaf b/tests/tap/specs/basic_operation.pgaf index 7dfae59db..7fc35993d 100644 --- a/tests/tap/specs/basic_operation.pgaf +++ b/tests/tap/specs/basic_operation.pgaf @@ -226,7 +226,8 @@ step test_014_writes_to_node1_fail { step test_015_fail_secondary { compose stop node1 - wait until node1 stopped timeout 30s + # 60s: compose stop can be slow on shared CI runners + wait until node1 stopped timeout 60s wait until node2 state is wait_primary timeout 60s } @@ -242,7 +243,8 @@ step test_016_drop_secondary { and node1 state is secondary timeout 90s exec node1 pg_autoctl drop node --no-wait - wait until node1 stopped timeout 30s + # 60s: compose stop can be slow on shared CI runners + wait until node1 stopped timeout 60s wait until node2 state is single timeout 90s sql node2 { SELECT slot_name @@ -359,8 +361,10 @@ step test_021_ifdown_primary { } step test_022_detect_network_partition { - wait until node2 state is demote_timeout timeout 90s - wait until node3 state is wait_primary timeout 90s + # 150s: demote_timeout transition involves monitor health-check cycles; + # shared CI runners can be slow to process the network partition signal + wait until node2 state is demote_timeout timeout 150s + wait until node3 state is wait_primary timeout 150s sleep 3s exec-fails node2 pg_autoctl inspect pgsetup ready sql node3 { SHOW synchronous_standby_names; } diff --git a/tests/tap/specs/enable_ssl.pgaf b/tests/tap/specs/enable_ssl.pgaf index d03d7440d..99e2fbbf4 100644 --- a/tests/tap/specs/enable_ssl.pgaf +++ b/tests/tap/specs/enable_ssl.pgaf @@ -63,9 +63,9 @@ step test_007_enable_ssl_secondary { exec node2 pg_autoctl enable ssl --ssl-self-signed --ssl-mode require compose stop node2 compose start node2 - exec node2 pg_autoctl inspect pgsetup wait --timeout 90 - sql node2 { SHOW ssl; } - expect { on } + # node2 is still in maintenance: the monitor assigned state is 'maintenance' + # so pg_autoctl will not start Postgres yet. Postgres starts in test_008 + # after disable maintenance triggers a state transition. } step test_008_disable_maintenance { @@ -73,6 +73,9 @@ step test_008_disable_maintenance { wait until node2 state is secondary and node1 state is primary timeout 90s + # 90s: wait for Postgres to be fully ready with SSL after the maintenance + # state transition on shared CI runners + exec node2 pg_autoctl inspect pgsetup wait --timeout 90 } step test_009_read_from_secondary { From a57d3c6e2b21789533d49fb30bd84453bcf21803 Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Tue, 7 Jul 2026 17:38:19 +0200 Subject: [PATCH 37/68] pgaftest: fix 4 CI failure patterns from run 28875391958 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four root causes diagnosed from the CI run logs; all fixed here. 1. upgrade/test_004_wait_convergence: 3-node post-upgrade reconvergence timed out at 180s on loaded shared CI runners. Bump to 300s. 2. node-extra/debian_clusters (structural): a. compose_gen.c: write_image_stanza_target() always used PGAF_IMAGE even when target='debian', so the pre-built run image was used instead of the Debian-cluster image. Now checks PGAF_DEBIAN_IMAGE for the 'debian' target; falls back to an inline build stanza. b. run-pgaftest.yml: build-images now also builds the 'debian' Docker target (one extra layer on top of 'run', fast via layer cache) and uploads it as a separate artifact. The test job downloads and loads it for the node-extra schedule, and passes PGVERSION and PGAF_DEBIAN_IMAGE to the runner. c. debian_clusters.pgaf test_002: hard-coded PG17 path replaced with $PGDATA (expanded by the shell inside the container at runtime). 3. citus-1/citus_skip_pg_hba: Docker healthcheck window (75s) too short for Citus coordinator init on a loaded runner (third Citus spec in the schedule). Increase retries 30→60 (new window: 15+60×2=135s). 4. multi-2/PG15 (two transient-state races): - multi_alternate test_005_003: 'demoted' is an intermediate state lasting under a second; the pgaftest assigned-state poller misses it on loaded runners. Drop the wait; go straight to the stable end state (secondary+primary), bump timeout to 300s. - multi_ifdown test_008: 'wait_primary' is likewise transient when the standby joins almost immediately. Merge both waits into a single stable-state wait at 300s. --- .github/workflows/run-pgaftest.yml | 43 ++++++++++++++++++++++++++-- src/bin/pgaftest/compose_gen.c | 38 +++++++++++++++++------- tests/tap/specs/debian_clusters.pgaf | 2 +- tests/tap/specs/multi_alternate.pgaf | 6 ++-- tests/tap/specs/multi_ifdown.pgaf | 6 ++-- tests/tap/specs/upgrade.pgaf | 3 +- 6 files changed, 80 insertions(+), 18 deletions(-) diff --git a/.github/workflows/run-pgaftest.yml b/.github/workflows/run-pgaftest.yml index 1031bae9a..0f66606e3 100644 --- a/.github/workflows/run-pgaftest.yml +++ b/.github/workflows/run-pgaftest.yml @@ -49,18 +49,42 @@ jobs: -t pgaf:run-pg${{ matrix.PGVERSION }} \ . - - name: Save image to tarball + - name: Build pgaf:debian image (PG${{ matrix.PGVERSION }}) + run: | + # Layer cache from the run build above covers the run stage; + # this step only adds the pg_createcluster layer on top. + case "${{ matrix.PGVERSION }}" in + 14|15) CITUSTAG=v12.1.5 ;; + *) CITUSTAG=v13.2.0 ;; + esac + docker build \ + --build-arg PGVERSION=${{ matrix.PGVERSION }} \ + --build-arg CITUSTAG=${CITUSTAG} \ + --target debian \ + -t pgaf:debian-pg${{ matrix.PGVERSION }} \ + . + + - name: Save images to tarballs run: | docker save pgaf:run-pg${{ matrix.PGVERSION }} \ | gzip > /tmp/pgaf-run-pg${{ matrix.PGVERSION }}.tar.gz + docker save pgaf:debian-pg${{ matrix.PGVERSION }} \ + | gzip > /tmp/pgaf-debian-pg${{ matrix.PGVERSION }}.tar.gz - - name: Upload image artifact + - name: Upload run image artifact uses: actions/upload-artifact@v7.0.1 with: name: pgaf-run-image-pg${{ matrix.PGVERSION }} path: /tmp/pgaf-run-pg${{ matrix.PGVERSION }}.tar.gz retention-days: 1 + - name: Upload debian image artifact + uses: actions/upload-artifact@v7.0.1 + with: + name: pgaf-debian-image-pg${{ matrix.PGVERSION }} + path: /tmp/pgaf-debian-pg${{ matrix.PGVERSION }}.tar.gz + retention-days: 1 + # --------------------------------------------------------------------------- # Build the pgaftest binary once from the PG17 build image. # The binary links against libpq at runtime, so it works with any PG version @@ -142,6 +166,19 @@ jobs: # Alias to the name pgaftest expects via PGAF_IMAGE docker tag pgaf:run-pg${{ matrix.PGVERSION }} pgaf:run + - name: Download pgaf:debian image (PG${{ matrix.PGVERSION }}) + if: matrix.schedule == 'node-extra' + uses: actions/download-artifact@v8.0.1 + with: + name: pgaf-debian-image-pg${{ matrix.PGVERSION }} + path: /tmp + + - name: Load pgaf:debian image into Docker + if: matrix.schedule == 'node-extra' + run: | + docker load < /tmp/pgaf-debian-pg${{ matrix.PGVERSION }}.tar.gz + docker tag pgaf:debian-pg${{ matrix.PGVERSION }} pgaf:debian + - name: Download pgaftest binary uses: actions/download-artifact@v8.0.1 with: @@ -163,6 +200,8 @@ jobs: timeout-minutes: 35 env: PGAF_IMAGE: pgaf:run + PGVERSION: ${{ matrix.PGVERSION }} + PGAF_DEBIAN_IMAGE: ${{ matrix.schedule == 'node-extra' && 'pgaf:debian' || '' }} run: | chmod +x /tmp/pgaftest-bin /tmp/pgaftest-bin run --schedule tests/tap/schedules/${{ matrix.schedule }}.sch diff --git a/src/bin/pgaftest/compose_gen.c b/src/bin/pgaftest/compose_gen.c index e37a5b4b4..d2543f624 100644 --- a/src/bin/pgaftest/compose_gen.c +++ b/src/bin/pgaftest/compose_gen.c @@ -359,16 +359,34 @@ write_image_stanza_target(FILE *f, const TestCluster *cluster, if (img && *img && !isTestRunner) { - fformat(f, " image: \"%s\"\n", img); - } - else - { - fformat(f, - " build:\n" - " context: \"%s\"\n" - " target: %s\n", - contextDir, target); + if (strcmp(target, "debian") == 0) + { + /* + * When PGAF_IMAGE is set (pre-built run image), the debian stage + * needs a separately built image supplied via PGAF_DEBIAN_IMAGE. + * If not provided, fall through to the inline build stanza so the + * debian target is built from source (slower, but correct). + */ + const char *debImg = getenv("PGAF_DEBIAN_IMAGE"); /* IGNORE-BANNED */ + + if (debImg && *debImg) + { + fformat(f, " image: \"%s\"\n", debImg); + return; + } + } + else + { + fformat(f, " image: \"%s\"\n", img); + return; + } } + + fformat(f, + " build:\n" + " context: \"%s\"\n" + " target: %s\n", + contextDir, target); } @@ -713,7 +731,7 @@ compose_gen_write(TestCluster *cluster, " \"--pgdata\", \"%s\"]\n" " interval: 2s\n" " timeout: 5s\n" - " retries: 30\n" + " retries: 60\n" " start_period: 15s\n", node_pgdata); } diff --git a/tests/tap/specs/debian_clusters.pgaf b/tests/tap/specs/debian_clusters.pgaf index 950a7310c..5366d3a52 100644 --- a/tests/tap/specs/debian_clusters.pgaf +++ b/tests/tap/specs/debian_clusters.pgaf @@ -36,7 +36,7 @@ step test_001_single_with_debian_cluster { # step test_002_conf_in_pgdata { - exec node1 test -f /var/lib/postgresql/17/main/postgresql.conf + exec node1 /bin/sh -c 'test -f ${PGDATA}/postgresql.conf' } # diff --git a/tests/tap/specs/multi_alternate.pgaf b/tests/tap/specs/multi_alternate.pgaf index 2855fdcee..5857f7484 100644 --- a/tests/tap/specs/multi_alternate.pgaf +++ b/tests/tap/specs/multi_alternate.pgaf @@ -161,10 +161,12 @@ step test_005_002_fail_primary_again { step test_005_003_bring_up_first_failed_primary { compose start node2 - wait until node2 state is demoted timeout 180s + # 'demoted' is a transient intermediate state that lasts under a second; + # waiting for it races reliably on shared CI runners. Wait for the stable + # end state instead. wait until node2 state is secondary and node3 state is primary - timeout 180s + timeout 300s } step test_005_004_bring_up_last_failed_primary { diff --git a/tests/tap/specs/multi_ifdown.pgaf b/tests/tap/specs/multi_ifdown.pgaf index bee3c057a..f37b24451 100644 --- a/tests/tap/specs/multi_ifdown.pgaf +++ b/tests/tap/specs/multi_ifdown.pgaf @@ -89,10 +89,12 @@ step test_007_insert_rows { step test_008_failover { network disconnect node1 network connect node3 - wait until node3 state is wait_primary timeout 180s + # 'wait_primary' is transient: node3 passes through it in under a second + # once node2 joins as standby. Skip the intermediate wait and go straight + # to the stable end state. 300s: shared CI runners can be slow. wait until node2 state is secondary and node3 state is primary - timeout 180s + timeout 300s sql node3 { SHOW synchronous_standby_names; } expect { ANY 1 (pgautofailover_standby_1, pgautofailover_standby_2) } } diff --git a/tests/tap/specs/upgrade.pgaf b/tests/tap/specs/upgrade.pgaf index 358dfdd96..d60f085b0 100644 --- a/tests/tap/specs/upgrade.pgaf +++ b/tests/tap/specs/upgrade.pgaf @@ -129,10 +129,11 @@ step test_003b_wait_keeper_restart { } step test_004_wait_convergence { + # 300s: 3-node post-upgrade reconvergence can be slow on shared CI runners wait until node1 state is primary and node2 state is secondary and node3 state is secondary - timeout 180s + timeout 300s } # ------------------------------------------------------------------------- From 0c7a8ee89b60069fdf5084fea490bd555c1384aa Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Tue, 7 Jul 2026 18:38:06 +0200 Subject: [PATCH 38/68] pgaftest: fix 6 more CI failures; consolidate schedules; restrict slow to PG17 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Spec timeout and transient-state fixes (six of seven diagnosed failures; report_lsn handled separately — see note below): * basic_operation.pgaf test_022: demote_timeout wait 150s → 300s. The demote_timeout state requires the monitor to exhaust its health-check retry budget against a silently-partitioned node; shared CI runners need more than 150 s for that. * ssl_cert.pgaf setup: 180s → 300s. verify-ca + cert-auth cluster formation is the slowest startup path (TLS handshake validation, ident map writing, CA-cert verification for every pg_autoctl connection) and is the third spec in the ssl schedule, running on a warm runner. * maintenance_and_drop.pgaf setup: 90s → 180s. Setup times out on loaded runners; this spec now runs later in the merged node schedule. * ensure.pgaf test_004_demoted: remove transient 'demoted' wait. Same fix as multi_alternate (committed in prior round): 'demoted' lasts under a second and the LISTEN/NOTIFY path cannot reliably catch it. Wait for the stable end state at 300s instead. * multi_maintenance.pgaf test_009b: 90s → 180s. Two nodes rejoining simultaneously (pg_rewind + replication restart each) can be slow. * compose_gen.c healthcheck: start_period 15s → 90s (retries already 60). New max window: 90 + 60×2 = 210s for Citus coordinator initialization. Note on report_lsn: runner_wait_notify_goal has a 'torn-read' SQL confirmation step that re-queries the monitor after a convergence notification. For transient states (report_lsn), the monitor may have already advanced by the time the SQL query executes, causing the function to retry forever and time out. The fix belongs in the runner (relax the SQL confirmation guard for convergence notifications that already carry both goal and reported state) not in the spec; tracked separately. Schedule consolidation (option 2): Merge monitor and node-extra into node. These schedules each took under 2 minutes and wasted 8 GitHub Actions runner slots per run. The merged node schedule now runs all of: create_standby_with_pgdata, maintenance_and_drop, auth, monitor_disabled, replace_monitor, extension_update, debian_clusters, tablespaces. PG version reduction (option 3): Slow schedules (multi-1, multi-2, citus-1, citus-2) now run PG17 only. These test pg_auto_failover FSM logic, not PG version-specific code paths. Result: 3×4 + 4×1 = 16 test jobs instead of 36, eliminating the 10-15 min queue wait that was doubling wall-clock time vs the old Python suite (which had only 4 jobs and got runners immediately). --- .github/workflows/run-pgaftest.yml | 52 +++++++++++++++-------- src/bin/pgaftest/compose_gen.c | 2 +- tests/tap/schedules/monitor.sch | 4 -- tests/tap/schedules/node-extra.sch | 3 -- tests/tap/schedules/node.sch | 9 +++- tests/tap/specs/basic_operation.pgaf | 6 +-- tests/tap/specs/ensure.pgaf | 5 ++- tests/tap/specs/maintenance_and_drop.pgaf | 3 +- tests/tap/specs/multi_maintenance.pgaf | 4 +- tests/tap/specs/ssl_cert.pgaf | 4 +- 10 files changed, 58 insertions(+), 34 deletions(-) delete mode 100644 tests/tap/schedules/monitor.sch delete mode 100644 tests/tap/schedules/node-extra.sch diff --git a/.github/workflows/run-pgaftest.yml b/.github/workflows/run-pgaftest.yml index 0f66606e3..9148c8c6f 100644 --- a/.github/workflows/run-pgaftest.yml +++ b/.github/workflows/run-pgaftest.yml @@ -127,9 +127,9 @@ jobs: retention-days: 1 # --------------------------------------------------------------------------- - # Run test schedules against every supported Postgres version. - # Each schedule groups several specs to reduce total job count from - # 19-specs × 4-PG = 76 jobs down to 6-schedules × 4-PG = 24 jobs. + # Run test schedules. Fast schedules (quick, node, ssl) run against all four + # Postgres versions. Slow schedules (multi-*, citus-*) run PG17 only. + # Total: 3×4 + 4×1 = 16 test jobs — keeps GitHub shared-runner queue short. # --------------------------------------------------------------------------- test: name: pgaftest / ${{ matrix.schedule }} (PG${{ matrix.PGVERSION }}) @@ -139,17 +139,35 @@ jobs: strategy: fail-fast: false matrix: - PGVERSION: [14, 15, 16, 17] - schedule: - - quick # basic_operation, basic_operation_listen_flag, config_get_set, skip_pg_hba - - node # create_standby_with_pgdata, maintenance_and_drop, auth - - node-extra # debian_clusters, tablespaces - - monitor # monitor_disabled, replace_monitor, extension_update - - ssl # enable_ssl, ssl_self_signed, ssl_cert - - multi-1 # multi_standbys, multi_maintenance, multi_async - - multi-2 # ensure, multi_alternate, multi_ifdown - - citus-1 # citus_cluster_name, citus_force_failover, citus_skip_pg_hba - - citus-2 # basic_citus_operation, nonha_citus_operation, citus_multi_standbys + # Fast/medium schedules run against all four Postgres versions. + # Slow schedules (multi-*, citus-*) run PG17 only: they exercise + # pg_auto_failover FSM logic, not Postgres version-specific code paths, + # and restricting them reduces total job count from 36 to 16 — avoiding + # GitHub Actions shared-runner queue saturation that was adding 10-15 min + # of queue wait to every run. + include: + # quick: basic_operation, basic_operation_listen_flag, config_get_set, skip_pg_hba + - { PGVERSION: 14, schedule: quick } + - { PGVERSION: 15, schedule: quick } + - { PGVERSION: 16, schedule: quick } + - { PGVERSION: 17, schedule: quick } + # node: create_standby_with_pgdata, maintenance_and_drop, auth, + # monitor_disabled, replace_monitor, extension_update, + # debian_clusters, tablespaces + - { PGVERSION: 14, schedule: node } + - { PGVERSION: 15, schedule: node } + - { PGVERSION: 16, schedule: node } + - { PGVERSION: 17, schedule: node } + # ssl: enable_ssl, ssl_self_signed, ssl_cert + - { PGVERSION: 14, schedule: ssl } + - { PGVERSION: 15, schedule: ssl } + - { PGVERSION: 16, schedule: ssl } + - { PGVERSION: 17, schedule: ssl } + # slow schedules: PG17 only + - { PGVERSION: 17, schedule: multi-1 } # multi_standbys, multi_maintenance, multi_async + - { PGVERSION: 17, schedule: multi-2 } # ensure, multi_alternate, multi_ifdown + - { PGVERSION: 17, schedule: citus-1 } # citus_cluster_name, citus_force_failover, citus_skip_pg_hba + - { PGVERSION: 17, schedule: citus-2 } # basic_citus_operation, nonha_citus_operation, citus_multi_standbys steps: - uses: actions/checkout@v7.0.0 @@ -167,14 +185,14 @@ jobs: docker tag pgaf:run-pg${{ matrix.PGVERSION }} pgaf:run - name: Download pgaf:debian image (PG${{ matrix.PGVERSION }}) - if: matrix.schedule == 'node-extra' + if: matrix.schedule == 'node' uses: actions/download-artifact@v8.0.1 with: name: pgaf-debian-image-pg${{ matrix.PGVERSION }} path: /tmp - name: Load pgaf:debian image into Docker - if: matrix.schedule == 'node-extra' + if: matrix.schedule == 'node' run: | docker load < /tmp/pgaf-debian-pg${{ matrix.PGVERSION }}.tar.gz docker tag pgaf:debian-pg${{ matrix.PGVERSION }} pgaf:debian @@ -201,7 +219,7 @@ jobs: env: PGAF_IMAGE: pgaf:run PGVERSION: ${{ matrix.PGVERSION }} - PGAF_DEBIAN_IMAGE: ${{ matrix.schedule == 'node-extra' && 'pgaf:debian' || '' }} + PGAF_DEBIAN_IMAGE: ${{ matrix.schedule == 'node' && 'pgaf:debian' || '' }} run: | chmod +x /tmp/pgaftest-bin /tmp/pgaftest-bin run --schedule tests/tap/schedules/${{ matrix.schedule }}.sch diff --git a/src/bin/pgaftest/compose_gen.c b/src/bin/pgaftest/compose_gen.c index d2543f624..8ea19cdf6 100644 --- a/src/bin/pgaftest/compose_gen.c +++ b/src/bin/pgaftest/compose_gen.c @@ -732,7 +732,7 @@ compose_gen_write(TestCluster *cluster, " interval: 2s\n" " timeout: 5s\n" " retries: 60\n" - " start_period: 15s\n", + " start_period: 90s\n", node_pgdata); } diff --git a/tests/tap/schedules/monitor.sch b/tests/tap/schedules/monitor.sch deleted file mode 100644 index 44ff1cca7..000000000 --- a/tests/tap/schedules/monitor.sch +++ /dev/null @@ -1,4 +0,0 @@ -# Monitor operations: disabled, replace, extension update (~15 min) -monitor_disabled -replace_monitor -extension_update diff --git a/tests/tap/schedules/node-extra.sch b/tests/tap/schedules/node-extra.sch deleted file mode 100644 index a1b45c542..000000000 --- a/tests/tap/schedules/node-extra.sch +++ /dev/null @@ -1,3 +0,0 @@ -# Debian-style split-config layout and tablespace HA (~15 min) -debian_clusters -tablespaces diff --git a/tests/tap/schedules/node.sch b/tests/tap/schedules/node.sch index be0d2ffe2..8c9960936 100644 --- a/tests/tap/schedules/node.sch +++ b/tests/tap/schedules/node.sch @@ -1,4 +1,11 @@ -# Node lifecycle: create, drop, auth (~10 min) +# Node lifecycle, monitor operations, and Debian/tablespace layouts (~30 min). +# Merged from former node, monitor, and node-extra schedules to reduce CI job +# count and GitHub Actions runner queue pressure. create_standby_with_pgdata maintenance_and_drop auth +monitor_disabled +replace_monitor +extension_update +debian_clusters +tablespaces diff --git a/tests/tap/specs/basic_operation.pgaf b/tests/tap/specs/basic_operation.pgaf index 7fc35993d..f5fbcd20c 100644 --- a/tests/tap/specs/basic_operation.pgaf +++ b/tests/tap/specs/basic_operation.pgaf @@ -361,10 +361,10 @@ step test_021_ifdown_primary { } step test_022_detect_network_partition { - # 150s: demote_timeout transition involves monitor health-check cycles; + # 300s: demote_timeout transition involves monitor health-check cycles; # shared CI runners can be slow to process the network partition signal - wait until node2 state is demote_timeout timeout 150s - wait until node3 state is wait_primary timeout 150s + wait until node2 state is demote_timeout timeout 300s + wait until node3 state is wait_primary timeout 300s sleep 3s exec-fails node2 pg_autoctl inspect pgsetup ready sql node3 { SHOW synchronous_standby_names; } diff --git a/tests/tap/specs/ensure.pgaf b/tests/tap/specs/ensure.pgaf index 1e24ed3a3..7594c0de1 100644 --- a/tests/tap/specs/ensure.pgaf +++ b/tests/tap/specs/ensure.pgaf @@ -51,10 +51,11 @@ step test_004_demoted { compose stop node1 sleep 30s compose start node1 - wait until node1 state is demoted timeout 180s + # 'demoted' is a sub-second transient state; waiting for it races on + # loaded shared CI runners. Wait for the stable end state instead. wait until node2 state is primary and node1 state is secondary - timeout 180s + timeout 300s } step test_005_inject_error_in_node2 { diff --git a/tests/tap/specs/maintenance_and_drop.pgaf b/tests/tap/specs/maintenance_and_drop.pgaf index b933b5035..1e30d3474 100644 --- a/tests/tap/specs/maintenance_and_drop.pgaf +++ b/tests/tap/specs/maintenance_and_drop.pgaf @@ -14,7 +14,8 @@ cluster { } setup { - wait until primary, secondary timeout 90s + # 180s: cluster formation can be slow on shared CI runners + wait until primary, secondary timeout 180s promote node1 } diff --git a/tests/tap/specs/multi_maintenance.pgaf b/tests/tap/specs/multi_maintenance.pgaf index 85a19ad69..cbf55bb95 100644 --- a/tests/tap/specs/multi_maintenance.pgaf +++ b/tests/tap/specs/multi_maintenance.pgaf @@ -145,10 +145,12 @@ step test_009a_enable_maintenance_on_primary_should_fail { step test_009b_disable_maintenance { exec node1 pg_autoctl disable maintenance exec node2 pg_autoctl disable maintenance + # 180s: two nodes rejoining simultaneously (pg_rewind + replication restart) + # can be slow on shared CI runners wait until node1 state is secondary and node2 state is secondary and node3 state is primary - timeout 90s + timeout 180s } step test_010_set_number_sync_standby_to_zero { diff --git a/tests/tap/specs/ssl_cert.pgaf b/tests/tap/specs/ssl_cert.pgaf index 7a49d6328..d107f09b7 100644 --- a/tests/tap/specs/ssl_cert.pgaf +++ b/tests/tap/specs/ssl_cert.pgaf @@ -19,9 +19,11 @@ cluster { } setup { + # 300s: verify-ca + cert-auth cluster formation is slower than plain auth + # on shared CI runners, especially as the third spec in the ssl schedule wait until node1 state is primary and node2 state is secondary - timeout 180s + timeout 300s promote node1 } From a9a47e4a411ddaab0b95d4942ec629b713d0d228 Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Tue, 7 Jul 2026 19:06:01 +0200 Subject: [PATCH 39/68] pgaftest: drain-at-start in runner_wait_notify_goal, remove SQL confirmation Transient FSM states (report_lsn, demoted, wait_primary) last less than a second. The old code issued a SQL confirmation after the LISTEN notification fired: by the time that round-trip returned, the monitor had already advanced past the transient target, so assigned_state and reported_state no longer matched and the wait fell through to a spurious timeout. Two changes fix this: 1. Drain-at-start: before the fast-path SQL check, drain any notifications already buffered in libpq. A preceding blocking exec command may have accumulated the entire FSM cycle while pgaftest waited for the subprocess. Draining on entry means a transient state that arrived and departed during the exec is not missed. It also prevents stale notifications from prior waits from satisfying the current one. 2. Remove SQL confirmation: once convergence is detected in the notification (goalState == reportedState == targetState), return immediately. The drain-at-start guarantee makes the stale-notification guard unnecessary, and the SQL round-trip is what caused the spurious timeout for transient states. monitor_wait_formation_states() already had drain-at-start (lines 1072-1075); this aligns the single-node path with the same pattern. --- src/bin/pgaftest/test_runner.c | 69 ++++++++++++++++++---------------- 1 file changed, 36 insertions(+), 33 deletions(-) diff --git a/src/bin/pgaftest/test_runner.c b/src/bin/pgaftest/test_runner.c index c0f528c34..c4421eb25 100644 --- a/src/bin/pgaftest/test_runner.c +++ b/src/bin/pgaftest/test_runner.c @@ -1347,6 +1347,28 @@ runner_wait_notify_goal(TestRunner *r, { runner_notify_connect(r); + /* + * Drain any notifications already buffered in libpq before the fast-path + * check. A preceding blocking exec command may have accumulated the + * entire FSM cycle in the socket while pgaftest was waiting for the + * subprocess to return. Processing those notifications here means that a + * transient state that arrived and departed during the exec is not missed. + * + * This also prevents stale convergence notifications from prior waits from + * firing the current wait: each wait drains the buffer on entry, so only + * notifications that arrive *after* the wait starts can satisfy it. + */ + if (r->notifyConnected) + { + bool satisfiedEarly = false; + + runner_drain_notify(r, NULL, &nodeName, &targetState, 1, &satisfiedEarly); + if (satisfiedEarly) + { + return true; + } + } + /* * Fast path: if the node has already converged on the target state * (reportedState == assignedState == targetState), return immediately. @@ -1421,12 +1443,19 @@ runner_wait_notify_goal(TestRunner *r, } /* - * Lift the wait only when both states have converged: - * the monitor assigned the target (goalState) and the - * node confirmed it (reportedState). The notification - * already carries both values so we can test here - * without an extra round-trip; we still do a final SQL - * confirmation to guard against a torn read. + * Lift the wait when both states have converged in the + * same notification: the monitor assigned the target + * (goalState) and the node confirmed it (reportedState). + * + * The drain-at-start above ensures that only + * notifications arriving after this wait began can + * reach this check, so there is no risk of a stale + * notification from a prior wait triggering a false + * positive. Trust the notification without a SQL + * round-trip: that confirmation would race against the + * monitor advancing past the target for transient states + * (report_lsn, demoted, wait_primary) and cause spurious + * timeouts. */ if (strcmp(ns_goal, targetState) == 0 && strcmp(ns_rep, targetState) == 0) @@ -1436,32 +1465,7 @@ runner_wait_notify_goal(TestRunner *r, /* post-match drain: no marking — belongs to next wait */ runner_drain_notify(r, NULL, NULL, NULL, 0, NULL); - /* final SQL: confirm both assigned and reported */ - char rep[64] = "", asgn[64] = ""; - if (monitor_get_node_state(r, nodeName, - rep, sizeof(rep), - asgn, sizeof(asgn))) - { - if (strcmp(rep, targetState) == 0 && - strcmp(asgn, targetState) == 0) - { - return true; - } - - /* - * Rare torn read: keep looping. - * The next notification will re-trigger. - */ - log_debug(" %s: notify shows convergence on %s " - "but SQL has rep=%s asgn=%s, retrying", - nodeName, targetState, rep, asgn); - } - else - { - /* can't reach monitor — trust the notification */ - return true; - } - goto next_iter; + return true; } } } @@ -1558,7 +1562,6 @@ runner_wait_notify_goal(TestRunner *r, } } -next_iter: { int remainMs = (int) ((deadline - time(NULL)) * 1000); if (remainMs <= 0) From 0702211f3d890d1720940d1e36d2a218abbac6ef Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Tue, 7 Jul 2026 20:26:38 +0200 Subject: [PATCH 40/68] pgaftest: fix test_022 demote_timeout wait for disconnected node MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit wait until node2 state is demote_timeout requires both goalState and reportedState to equal demote_timeout. A network-disconnected node can never report back to the monitor, so reportedState stays at 'primary' permanently and the wait always times out. Change to 'wait until node2 assigned-state = demote_timeout', which only checks the monitor's assignment (goalState). The monitor does assign demote_timeout within ~20s of the network disconnect; the disconnected node's inability to confirm is irrelevant to the test's purpose of verifying that the monitor detected the partition. Also increase auth.pgaf setup timeout 180s → 300s: md5 password auth adds overhead to cluster formation; 180s is too tight on loaded shared CI runners. --- tests/tap/specs/auth.pgaf | 2 +- tests/tap/specs/basic_operation.pgaf | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/tap/specs/auth.pgaf b/tests/tap/specs/auth.pgaf index ab7e5fa4b..3e33923ae 100644 --- a/tests/tap/specs/auth.pgaf +++ b/tests/tap/specs/auth.pgaf @@ -32,7 +32,7 @@ cluster { setup { # 180s: cert-auth cluster formation is slower on shared CI runners - wait until primary, secondary timeout 180s + wait until primary, secondary timeout 300s promote node1 } diff --git a/tests/tap/specs/basic_operation.pgaf b/tests/tap/specs/basic_operation.pgaf index f5fbcd20c..f9fe83d41 100644 --- a/tests/tap/specs/basic_operation.pgaf +++ b/tests/tap/specs/basic_operation.pgaf @@ -361,9 +361,10 @@ step test_021_ifdown_primary { } step test_022_detect_network_partition { - # 300s: demote_timeout transition involves monitor health-check cycles; - # shared CI runners can be slow to process the network partition signal - wait until node2 state is demote_timeout timeout 300s + # node2 is network-disconnected so it cannot report its state back to the + # monitor. Check only the assigned state (monitor's decision); checking + # the full convergence state would wait forever for a node that is offline. + wait until node2 assigned-state = demote_timeout timeout 300s wait until node3 state is wait_primary timeout 300s sleep 3s exec-fails node2 pg_autoctl inspect pgsetup ready From 9dc746473d871ece4dc3686bcfbaa04a7eeac3cd Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Tue, 7 Jul 2026 20:26:52 +0200 Subject: [PATCH 41/68] pgaftest: ssl client cert for monitor LISTEN; wider healthcheck window MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two independent fixes: 1. ssl client cert for monitor LISTEN connection When the cluster uses verify-ca/verify-full SSL with cert auth, the monitor's pg_hba.conf requires a client certificate for every connection — including the runner's own libpq connection. Without one, runner_notify_connect() fails silently, LISTEN is unavailable, and the SQL-polling fallback also fails, so wait-for-state commands always timeout regardless of actual cluster state. Add sslmode, sslrootcert, sslcert, sslkey parameters to the monitor connection string when ssl is verify-ca/verify-full and auth is cert. The runner already generates the client cert at /ssl/client/ via compose_gen_write_ssl_certs(); those paths are now used here. 2. Wider Docker healthcheck window for data nodes The previous window was 90s start_period + 60 × 2s retries = 210s. The third spec in the citus-1 schedule (citus_skip_pg_hba) exhausted this window (coord0a became unhealthy at ~212s) on a loaded CI runner. Increase to start_period=120s + 120 × 2s retries = 360s to give containers more headroom on shared runners without affecting fast-start containers (Docker accepts 'healthy' as soon as the first check passes, regardless of start_period). --- src/bin/pgaftest/compose_gen.c | 4 ++-- src/bin/pgaftest/test_runner.c | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/bin/pgaftest/compose_gen.c b/src/bin/pgaftest/compose_gen.c index 8ea19cdf6..e3e08aa16 100644 --- a/src/bin/pgaftest/compose_gen.c +++ b/src/bin/pgaftest/compose_gen.c @@ -731,8 +731,8 @@ compose_gen_write(TestCluster *cluster, " \"--pgdata\", \"%s\"]\n" " interval: 2s\n" " timeout: 5s\n" - " retries: 60\n" - " start_period: 90s\n", + " retries: 120\n" + " start_period: 120s\n", node_pgdata); } diff --git a/src/bin/pgaftest/test_runner.c b/src/bin/pgaftest/test_runner.c index c4421eb25..663a3b1d3 100644 --- a/src/bin/pgaftest/test_runner.c +++ b/src/bin/pgaftest/test_runner.c @@ -776,6 +776,26 @@ runner_notify_connect(TestRunner *r) "connect_timeout=5", port, cl->monitorPassword); } + else if ((strcmp(cl->ssl, "verify-ca") == 0 || + strcmp(cl->ssl, "verify-full") == 0) && + strcmp(cl->auth, "cert") == 0) + { + /* + * The monitor requires client certificate authentication: supply + * the runner's own client cert (generated alongside the spec's + * server certs) so that both LISTEN and SQL-fallback paths can + * connect. Without this the runner logs "LISTEN not available" + * and the wait-for-state fallback also fails. + */ + sformat(connstr, sizeof(connstr), + "host=localhost port=%d dbname=pg_auto_failover " + "user=autoctl_node connect_timeout=5 " + "sslmode=verify-ca " + "sslrootcert=%s/ssl/ca.crt " + "sslcert=%s/ssl/client/postgresql.crt " + "sslkey=%s/ssl/client/postgresql.key", + port, r->workDir, r->workDir, r->workDir); + } else { sformat(connstr, sizeof(connstr), From efd3fdff3f8806dc6493c39b1c53eab4d29ee301 Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Tue, 7 Jul 2026 20:52:10 +0200 Subject: [PATCH 42/68] pgaftest: fix ssl client key permissions for runner LISTEN connection The client private key at ssl/client/postgresql.key is generated with mode 0644 so the container user (different UID from the CI runner) can read it from the read-only bind-mount. libpq enforces strict key permissions and refuses to use a key with group/world read access, causing the LISTEN connection to fail silently. Copy the key to ssl/client/runner.key at mode 0600 before opening the libpq connection. The copy is done once (guarded by access(2)) and reused across reconnect attempts, so there is no per-retry overhead. --- src/bin/pgaftest/test_runner.c | 38 ++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/src/bin/pgaftest/test_runner.c b/src/bin/pgaftest/test_runner.c index 663a3b1d3..7320f4398 100644 --- a/src/bin/pgaftest/test_runner.c +++ b/src/bin/pgaftest/test_runner.c @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -786,15 +787,48 @@ runner_notify_connect(TestRunner *r) * server certs) so that both LISTEN and SQL-fallback paths can * connect. Without this the runner logs "LISTEN not available" * and the wait-for-state fallback also fails. + * + * The generated client key is chmod 0644 so the container user + * can read it from the bind-mount. libpq refuses private keys + * with group/world read access, so copy it to a 0600 temp file + * that only the runner process can read. */ + char srcKey[MAXPGPATH], runnerKey[MAXPGPATH]; + sformat(srcKey, sizeof(srcKey), + "%s/ssl/client/postgresql.key", r->workDir); + sformat(runnerKey, sizeof(runnerKey), + "%s/ssl/client/runner.key", r->workDir); + + /* copy src → runner.key at 0600 if not already done */ + if (access(runnerKey, F_OK) != 0) + { + int src = open(srcKey, O_RDONLY); + if (src >= 0) + { + int dst = open(runnerKey, + O_WRONLY | O_CREAT | O_TRUNC, 0600); + if (dst >= 0) + { + char buf[4096]; + ssize_t n; + while ((n = read(src, buf, sizeof(buf))) > 0) + { + (void) write(dst, buf, n); + } + close(dst); + } + close(src); + } + } + sformat(connstr, sizeof(connstr), "host=localhost port=%d dbname=pg_auto_failover " "user=autoctl_node connect_timeout=5 " "sslmode=verify-ca " "sslrootcert=%s/ssl/ca.crt " "sslcert=%s/ssl/client/postgresql.crt " - "sslkey=%s/ssl/client/postgresql.key", - port, r->workDir, r->workDir, r->workDir); + "sslkey=%s", + port, r->workDir, r->workDir, runnerKey); } else { From 67a3548f4cb8630ec670d25a5af8c708af552982 Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Tue, 7 Jul 2026 21:21:04 +0200 Subject: [PATCH 43/68] keeper: use cert map=pgautofailover for replication HBA with cert auth When auth=cert is configured, pg_autoctl writes HBA rules for standby replication connections with the plain 'cert' method. Cert auth requires the client certificate CN to match the database username, but both monitor and replication connections use the same client certificate (CN=autoctl_node) while the replication role is pgautofailover_replicator. Without an ident map the replication connection always fails: FATAL: certificate authentication failed for user "pgautofailover_replicator" Fix keeper_update_group_hba() to: 1. Use 'cert map=pgautofailover' for the replication HBA rule, matching the same pattern already used in cli_do_misc.c for LAN CIDR rules. 2. Call pghba_ensure_ident_map_entry() to write pgautofailover autoctl_node pgautofailover_replicator to pg_ident.conf, so the cert CN maps to the replication role. Verified with ssl_cert.pgaf: all 5 tests pass, formation completes in ~5 seconds. --- src/bin/pg_autoctl/keeper.c | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/bin/pg_autoctl/keeper.c b/src/bin/pg_autoctl/keeper.c index 782e0d1e5..d0cb4e351 100644 --- a/src/bin/pg_autoctl/keeper.c +++ b/src/bin/pg_autoctl/keeper.c @@ -2078,12 +2078,33 @@ keeper_update_group_hba(Keeper *keeper, NodeAddressArray *diffNodesArray) sformat(hbaFilePath, MAXPGPATH, "%s/pg_hba.conf", postgresSetup->pgdata); + /* + * With cert auth, replication connections from standbys use the same + * client certificate (CN=autoctl_node) as monitor connections. The + * database user for replication is pgautofailover_replicator, so cert + * auth needs an ident map to bridge the two names. + */ + bool isCert = (strcmp(authMethod, "cert") == 0); + const char *replAuth = isCert ? "cert map=pgautofailover" : authMethod; + + if (isCert) + { + if (!pghba_ensure_ident_map_entry(postgresSetup->pgdata, + "pgautofailover", + PG_AUTOCTL_MONITOR_USERNAME, + PG_AUTOCTL_REPLICA_USERNAME)) + { + log_error("Failed to add cert ident map entry to pg_ident.conf"); + return false; + } + } + if (!pghba_ensure_host_rules_exist(hbaFilePath, diffNodesArray, postgresSetup->ssl.active, postgresSetup->dbname, PG_AUTOCTL_REPLICA_USERNAME, - authMethod, + replAuth, keeper->config.pgSetup.hbaLevel)) { log_error("Failed to edit HBA file \"%s\" to update rules to current " From a2ce2b613a53897bf148a0d0931b775676a1d5dc Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Tue, 7 Jul 2026 22:28:35 +0200 Subject: [PATCH 44/68] Fix upstream_has_replication_slot() to authenticate with md5 When --auth md5 is used, upstream_has_replication_slot() needs to supply the replication password to connect to the upstream node. Previously it built the connection string without a password, causing authentication failures when the upstream requires md5. After 5 consecutive failures wait_until_primary_has_created_our_replication_slot() gives up, the init process exits, the supervisor restarts it up to 5 times in 300s, and the container dies -- all while the test waits for the formation to reach primary/secondary, timing out after 300s. Fix: set PGPASSWORD from upstream->password before calling pgsql_init(), following the same save/restore pattern used in pg_basebackup(). --- src/bin/pg_autoctl/primary_standby.c | 41 ++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/src/bin/pg_autoctl/primary_standby.c b/src/bin/pg_autoctl/primary_standby.c index 96a171978..d433e46e5 100644 --- a/src/bin/pg_autoctl/primary_standby.c +++ b/src/bin/pg_autoctl/primary_standby.c @@ -13,6 +13,7 @@ #include "postgres_fe.h" #include "config.h" +#include "env_utils.h" #include "file_utils.h" #include "keeper.h" #include "log.h" @@ -440,6 +441,8 @@ upstream_has_replication_slot(ReplicationSource *upstream, PostgresSetup upstreamSetup = { 0 }; PGSQL upstreamClient = { 0 }; char connectionString[MAXCONNINFO] = { 0 }; + char savedPgPassword[BUFSIZE] = { 0 }; + bool passwordSet = false; /* prepare a PostgresSetup that allows preparing a connection string */ strlcpy(upstreamSetup.username, PG_AUTOCTL_REPLICA_USERNAME, NAMEDATALEN); @@ -454,23 +457,45 @@ upstream_has_replication_slot(ReplicationSource *upstream, */ pg_setup_get_local_connection_string(&upstreamSetup, connectionString); - if (!pgsql_init(&upstreamClient, connectionString, PGSQL_CONN_UPSTREAM)) + /* + * When a replication password is configured (e.g. --auth md5), supply it + * via PGPASSWORD so that the connection to the upstream node can + * authenticate. Save and restore any existing value. + */ + if (!IS_EMPTY_STRING_BUFFER(upstream->password)) { - /* errors have already been logged */ - return false; + if (env_exists("PGPASSWORD")) + { + if (!get_env_copy("PGPASSWORD", savedPgPassword, sizeof(savedPgPassword))) + { + return false; + } + } + setenv("PGPASSWORD", upstream->password, 1); + passwordSet = true; } - if (!pgsql_replication_slot_exists(&upstreamClient, - upstream->slotName, - hasReplicationSlot)) + if (!pgsql_init(&upstreamClient, connectionString, PGSQL_CONN_UPSTREAM)) { /* errors have already been logged */ - PQfinish(upstreamClient.connection); + if (passwordSet) + { + setenv("PGPASSWORD", savedPgPassword, 1); + } return false; } + bool result = pgsql_replication_slot_exists(&upstreamClient, + upstream->slotName, + hasReplicationSlot); PQfinish(upstreamClient.connection); - return true; + + if (passwordSet) + { + setenv("PGPASSWORD", savedPgPassword, 1); + } + + return result; } From 603a5e81252a6ce495155f2b7c17a26e84821d7b Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Tue, 7 Jul 2026 23:19:54 +0200 Subject: [PATCH 45/68] CI: run pgaftest inside pgaf:pgaftest container (Docker-out-of-Docker) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the bare-runner pattern (extract binary + apt-get install libpq5) with a self-contained pgaf:pgaftest Docker image that already bundles libpq5, docker-ce-cli, and docker-compose-plugin. build-pgaftest now builds --target pgaftest and uploads the image as an artifact. All test/installcheck/upgrade jobs load that image and run pgaftest via: docker run --rm \ -v /var/run/docker.sock:/var/run/docker.sock \ -v /tmp/pgaftest:/tmp/pgaftest \ -v $(pwd):/work:ro \ pgaf:pgaftest \ pgaftest run ... The /tmp/pgaftest bind-mount lets compose stacks started by pgaftest (inside the container) access the generated INI and compose files via the same host paths the Docker daemon sees — the standard DooD pattern on Linux runners. This eliminates the fragile dependency on Microsoft's apt repositories that are pre-installed on GitHub's ubuntu-latest image and have been causing sporadic apt-get update failures (NOSPLIT / auth errors) that have nothing to do with our code. --- .github/workflows/run-pgaftest.yml | 123 ++++++++++++++--------------- 1 file changed, 58 insertions(+), 65 deletions(-) diff --git a/.github/workflows/run-pgaftest.yml b/.github/workflows/run-pgaftest.yml index 9148c8c6f..79bbf7d0f 100644 --- a/.github/workflows/run-pgaftest.yml +++ b/.github/workflows/run-pgaftest.yml @@ -86,12 +86,16 @@ jobs: retention-days: 1 # --------------------------------------------------------------------------- - # Build the pgaftest binary once from the PG17 build image. - # The binary links against libpq at runtime, so it works with any PG version - # as long as the matching libpq5 package is installed on the runner. + # Build the pgaf:pgaftest image once from the PG17 build image. + # The Dockerfile pgaftest target bundles the pgaftest binary together with + # its runtime dependencies (libpq5 from PGDG, docker-ce-cli, and the + # docker-compose-plugin) so that test jobs run pgaftest inside this + # container via Docker-out-of-Docker rather than installing libpq on the + # bare runner. That eliminates the fragile apt-get dependency on the + # pre-installed Microsoft package repositories on GitHub's runner image. # --------------------------------------------------------------------------- build-pgaftest: - name: Build pgaftest binary + name: Build pgaftest image runs-on: ubuntu-latest env: PGVERSION: 17 @@ -102,28 +106,23 @@ jobs: - name: Generate git-version.h run: make version - - name: Build pg_auto_failover build image + - name: Build pgaf:pgaftest image (PG${{ env.PGVERSION }}) run: | docker build \ --build-arg PGVERSION=${{ env.PGVERSION }} \ --build-arg CITUSTAG=v13.2.0 \ - --target build \ - -t pgaf:build \ + --target pgaftest \ + -t pgaf:pgaftest \ . - - name: Extract pgaftest binary - run: | - docker create --name pgaftest-extract pgaf:build - docker cp pgaftest-extract:/usr/lib/postgresql/${{ env.PGVERSION }}/bin/pgaftest \ - /tmp/pgaftest-bin - docker rm pgaftest-extract - chmod +x /tmp/pgaftest-bin + - name: Save image to tarball + run: docker save pgaf:pgaftest | gzip > /tmp/pgaf-pgaftest.tar.gz - - name: Upload pgaftest binary + - name: Upload pgaftest image artifact uses: actions/upload-artifact@v7.0.1 with: - name: pgaftest-binary - path: /tmp/pgaftest-bin + name: pgaftest-image + path: /tmp/pgaf-pgaftest.tar.gz retention-days: 1 # --------------------------------------------------------------------------- @@ -197,32 +196,29 @@ jobs: docker load < /tmp/pgaf-debian-pg${{ matrix.PGVERSION }}.tar.gz docker tag pgaf:debian-pg${{ matrix.PGVERSION }} pgaf:debian - - name: Download pgaftest binary + - name: Download pgaf:pgaftest image uses: actions/download-artifact@v8.0.1 with: - name: pgaftest-binary + name: pgaftest-image path: /tmp - - name: Install libpq runtime (PG${{ matrix.PGVERSION }}) - run: | - sudo apt-get install -y curl ca-certificates gnupg - curl https://www.postgresql.org/media/keys/ACCC4CF8.asc \ - | sudo apt-key add - - echo "deb http://apt.postgresql.org/pub/repos/apt \ - $(lsb_release -cs)-pgdg main" \ - | sudo tee /etc/apt/sources.list.d/pgdg.list - sudo apt-get update - sudo apt-get install -y libpq5 + - name: Load pgaf:pgaftest image into Docker + run: docker load < /tmp/pgaf-pgaftest.tar.gz - name: Run schedule ${{ matrix.schedule }} timeout-minutes: 35 - env: - PGAF_IMAGE: pgaf:run - PGVERSION: ${{ matrix.PGVERSION }} - PGAF_DEBIAN_IMAGE: ${{ matrix.schedule == 'node' && 'pgaf:debian' || '' }} run: | - chmod +x /tmp/pgaftest-bin - /tmp/pgaftest-bin run --schedule tests/tap/schedules/${{ matrix.schedule }}.sch + mkdir -p /tmp/pgaftest + docker run --rm \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v /tmp/pgaftest:/tmp/pgaftest \ + -v "$(pwd)":/work:ro \ + -w /work \ + -e PGAF_IMAGE=pgaf:run \ + -e PGVERSION=${{ matrix.PGVERSION }} \ + -e PGAF_DEBIAN_IMAGE=${{ matrix.schedule == 'node' && 'pgaf:debian' || '' }} \ + pgaf:pgaftest \ + pgaftest run --schedule tests/tap/schedules/${{ matrix.schedule }}.sch # --------------------------------------------------------------------------- # installcheck: SQL regression suite via pg_regress. @@ -242,23 +238,15 @@ jobs: steps: - uses: actions/checkout@v7.0.0 - - name: Install libpq runtime - run: | - sudo apt-get install -y curl ca-certificates gnupg - curl https://www.postgresql.org/media/keys/ACCC4CF8.asc \ - | sudo apt-key add - - echo "deb http://apt.postgresql.org/pub/repos/apt \ - $(lsb_release -cs)-pgdg main" \ - | sudo tee /etc/apt/sources.list.d/pgdg.list - sudo apt-get update - sudo apt-get install -y libpq5 - - - name: Download pgaftest binary + - name: Download pgaf:pgaftest image uses: actions/download-artifact@v8.0.1 with: - name: pgaftest-binary + name: pgaftest-image path: /tmp + - name: Load pgaf:pgaftest image into Docker + run: docker load < /tmp/pgaf-pgaftest.tar.gz + - name: Generate git-version.h run: make version @@ -278,8 +266,14 @@ jobs: - name: Run installcheck spec timeout-minutes: 15 run: | - chmod +x /tmp/pgaftest-bin - /tmp/pgaftest-bin run tests/tap/specs/installcheck.pgaf + mkdir -p /tmp/pgaftest + docker run --rm \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v /tmp/pgaftest:/tmp/pgaftest \ + -v "$(pwd)":/work:ro \ + -w /work \ + pgaf:pgaftest \ + pgaftest run tests/tap/specs/installcheck.pgaf # --------------------------------------------------------------------------- # Upgrade test: binary + extension swap without container restart. @@ -300,23 +294,15 @@ jobs: # Full history needed so PREV_TAG auto-detection finds the right tag. fetch-depth: 0 - - name: Install libpq runtime - run: | - sudo apt-get install -y curl ca-certificates gnupg - curl https://www.postgresql.org/media/keys/ACCC4CF8.asc \ - | sudo apt-key add - - echo "deb http://apt.postgresql.org/pub/repos/apt \ - $(lsb_release -cs)-pgdg main" \ - | sudo tee /etc/apt/sources.list.d/pgdg.list - sudo apt-get update - sudo apt-get install -y libpq5 - - - name: Download pgaftest binary + - name: Download pgaf:pgaftest image uses: actions/download-artifact@v8.0.1 with: - name: pgaftest-binary + name: pgaftest-image path: /tmp + - name: Load pgaf:pgaftest image into Docker + run: docker load < /tmp/pgaf-pgaftest.tar.gz + - name: Generate git-version.h run: make version @@ -329,5 +315,12 @@ jobs: - name: Run upgrade spec timeout-minutes: 25 run: | - chmod +x /tmp/pgaftest-bin - /tmp/pgaftest-bin run tests/tap/specs/upgrade.pgaf + mkdir -p /tmp/pgaftest + docker run --rm \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v /tmp/pgaftest:/tmp/pgaftest \ + -v "$(pwd)":/work:ro \ + -w /work \ + -e PGVERSION=${{ env.PGVERSION }} \ + pgaf:pgaftest \ + pgaftest run tests/tap/specs/upgrade.pgaf From eee4e9e0a1d5ba44378b51d7fbefe5de359fd4f5 Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Tue, 7 Jul 2026 23:35:27 +0200 Subject: [PATCH 46/68] CI: fix docker socket access in pgaf:pgaftest container pgaf:pgaftest runs as USER docker (UID 1000). On GitHub's ubuntu-latest runners /var/run/docker.sock is srw-rw---- root:docker where the docker GID is ~999, not 1000. The container's docker group GID doesn't match the host GID so all docker compose calls fail immediately with permission denied, causing every test job to exit in <30 seconds. Fix: run the container as root and add the host docker socket GID via --group-add so both the socket access and writes to /tmp/pgaftest (bind-mounted from the runner) work without restriction. --- .github/workflows/run-pgaftest.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/run-pgaftest.yml b/.github/workflows/run-pgaftest.yml index 79bbf7d0f..34edde714 100644 --- a/.github/workflows/run-pgaftest.yml +++ b/.github/workflows/run-pgaftest.yml @@ -209,7 +209,10 @@ jobs: timeout-minutes: 35 run: | mkdir -p /tmp/pgaftest + DOCKER_GID=$(stat -c '%g' /var/run/docker.sock) docker run --rm \ + --user root \ + --group-add "${DOCKER_GID}" \ -v /var/run/docker.sock:/var/run/docker.sock \ -v /tmp/pgaftest:/tmp/pgaftest \ -v "$(pwd)":/work:ro \ @@ -267,7 +270,10 @@ jobs: timeout-minutes: 15 run: | mkdir -p /tmp/pgaftest + DOCKER_GID=$(stat -c '%g' /var/run/docker.sock) docker run --rm \ + --user root \ + --group-add "${DOCKER_GID}" \ -v /var/run/docker.sock:/var/run/docker.sock \ -v /tmp/pgaftest:/tmp/pgaftest \ -v "$(pwd)":/work:ro \ @@ -316,7 +322,10 @@ jobs: timeout-minutes: 25 run: | mkdir -p /tmp/pgaftest + DOCKER_GID=$(stat -c '%g' /var/run/docker.sock) docker run --rm \ + --user root \ + --group-add "${DOCKER_GID}" \ -v /var/run/docker.sock:/var/run/docker.sock \ -v /tmp/pgaftest:/tmp/pgaftest \ -v "$(pwd)":/work:ro \ From e393a46c4dafba80a30910c6f8f399469bd434bc Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Tue, 7 Jul 2026 23:39:41 +0200 Subject: [PATCH 47/68] CI: chmod 777 /tmp/pgaftest before docker run GitHub Actions ubuntu-latest now uses rootless Docker. Container root (uid 0) maps to the rootless daemon uid on the host, which is different from the runner uid that created /tmp/pgaftest (mode 755). Writes from inside the pgaf:pgaftest container therefore fail with EACCES when pgaftest tries to create its per-spec work directories. Making the bind-mount root world-writable (777) allows any mapped uid to create sub-directories inside it, restoring write access regardless of the uid remapping in effect. --- .github/workflows/run-pgaftest.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/run-pgaftest.yml b/.github/workflows/run-pgaftest.yml index 34edde714..5de99bac2 100644 --- a/.github/workflows/run-pgaftest.yml +++ b/.github/workflows/run-pgaftest.yml @@ -208,7 +208,7 @@ jobs: - name: Run schedule ${{ matrix.schedule }} timeout-minutes: 35 run: | - mkdir -p /tmp/pgaftest + mkdir -p /tmp/pgaftest && chmod 777 /tmp/pgaftest DOCKER_GID=$(stat -c '%g' /var/run/docker.sock) docker run --rm \ --user root \ @@ -269,7 +269,7 @@ jobs: - name: Run installcheck spec timeout-minutes: 15 run: | - mkdir -p /tmp/pgaftest + mkdir -p /tmp/pgaftest && chmod 777 /tmp/pgaftest DOCKER_GID=$(stat -c '%g' /var/run/docker.sock) docker run --rm \ --user root \ @@ -321,7 +321,7 @@ jobs: - name: Run upgrade spec timeout-minutes: 25 run: | - mkdir -p /tmp/pgaftest + mkdir -p /tmp/pgaftest && chmod 777 /tmp/pgaftest DOCKER_GID=$(stat -c '%g' /var/run/docker.sock) docker run --rm \ --user root \ From c28db095909feda757b7f1254ee619e1ef5ce3b6 Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Wed, 8 Jul 2026 00:03:35 +0200 Subject: [PATCH 48/68] tests: fix test_024_stop_postgres_monitor in basic_operation assert stays primary while { stop postgres monitor } fails because the stays-while logic checks the state via the monitor (LISTEN or SQL) after each body command. When the body stops the monitor's own postgres, both the LISTEN connection and the SQL fallback become unavailable, so the runner reports 'could not get state of node3 after command' and the assertion fails incorrectly. The monitor cannot assign state changes while its postgres is down, so there is nothing to guard against during the outage. Replace the stays-while block with a plain stop + sleep + wait, matching the intent of the original Python test: verify that a brief monitor outage does not trigger a spurious failover. --- tests/tap/specs/basic_operation.pgaf | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/tap/specs/basic_operation.pgaf b/tests/tap/specs/basic_operation.pgaf index f9fe83d41..43b809901 100644 --- a/tests/tap/specs/basic_operation.pgaf +++ b/tests/tap/specs/basic_operation.pgaf @@ -389,10 +389,13 @@ step test_023_ifup_old_primary { # step test_024_stop_postgres_monitor { - assert node3 stays primary while { - stop postgres monitor - sleep 5s - } + # Stop the monitor's postgres directly. The monitor cannot assign state + # changes while postgres is down, so there is no point wrapping this in + # "assert stays primary while" — that construct queries the monitor to + # verify the state, which is impossible when the monitor is the one being + # stopped. The subsequent wait verifies recovery. + stop postgres monitor + sleep 5s wait until node3 state is primary timeout 60s } From 53d7f36017d2fcbfc0d38063fa3ebe81836c378d Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Wed, 8 Jul 2026 01:24:10 +0200 Subject: [PATCH 49/68] Fix three CI failures: multi_ifdown, citus_skip_pg_hba, deferred healthcheck multi_ifdown test_014: compose stop node3/node1 caused pg_autoctl perform failover to block ~68s waiting for dead primaries, leaving only 22s of a 90s timeout. Switch to network disconnect (matches the Python test's ifdown()), run the failover in background so the wait starts immediately, and increase the timeout to 300s. Update test_015 to use network connect instead of compose start. citus_skip_pg_hba: coord0a started simultaneously with the monitor but auth=skip leaves the monitor's pg_hba.conf with only localhost rules, so coord0a's registration attempts from another container all fail and its healthcheck never passes. Make coord0a launch deferred; move the monitor HBA setup into the setup block (before starting coord0a); rename test_000 to reflect its new sole job of adding coord0a's own HBA rules. compose_gen.c: a deferred node in the first slot (now coord0a) was incorrectly chosen as the healthcheck anchor. Its pg_autoctl status always returns non-zero during sleep infinity, blocking all subsequent nodes via service_healthy. Skip deferred nodes when selecting firstNode so the healthcheck lands on the first node that actually starts postgres. --- src/bin/pgaftest/compose_gen.c | 4 ++-- tests/tap/specs/citus_skip_pg_hba.pgaf | 27 +++++++++++++------------- tests/tap/specs/multi_ifdown.pgaf | 16 ++++++--------- 3 files changed, 21 insertions(+), 26 deletions(-) diff --git a/src/bin/pgaftest/compose_gen.c b/src/bin/pgaftest/compose_gen.c index e3e08aa16..2d70a0a4d 100644 --- a/src/bin/pgaftest/compose_gen.c +++ b/src/bin/pgaftest/compose_gen.c @@ -723,7 +723,7 @@ compose_gen_write(TestCluster *cluster, * nodes use service_started so they launch as soon as node1 has * started (they don't need postgres to be ready yet). */ - if (!firstNode && cluster->withMonitor) + if (!firstNode && cluster->withMonitor && !n->launchDeferred) { fformat(f, " healthcheck:\n" @@ -747,7 +747,7 @@ compose_gen_write(TestCluster *cluster, } fformat(f, "\n"); - if (!firstNode) + if (!firstNode && !n->launchDeferred) { firstNode = n; } diff --git a/tests/tap/specs/citus_skip_pg_hba.pgaf b/tests/tap/specs/citus_skip_pg_hba.pgaf index 055aa64dc..2c65a4825 100644 --- a/tests/tap/specs/citus_skip_pg_hba.pgaf +++ b/tests/tap/specs/citus_skip_pg_hba.pgaf @@ -1,27 +1,25 @@ # Test Citus cluster with authMethod=skip (pg_autoctl does not edit pg_hba.conf). # # With auth=skip, pg_autoctl leaves pg_hba.conf untouched after postgres init. -# The coordinator pair starts normally (coord0b pg_basebackup-s from coord0a -# and inherits its HBA; the monitor and coordinator connections work via the -# Docker network with trust auth set at the monitor level). +# All data nodes are launch deferred so startup can be sequenced manually: # -# worker1a is launch deferred. The first manual run fails because the -# coordinator tries master_activate_node on worker1a but worker1a's default -# pg_hba.conf blocks that connection. After we manually append the required -# HBA rules and retry, activation succeeds — and pg_autoctl has not modified -# pg_hba.conf itself (verified by assert hba-edited = false). +# setup: monitor starts → add LAN HBA to monitor → start coord0a +# test_000: add LAN HBA to coord0a (so coord0b can connect later) +# test_001a: wait until coord0a reaches single +# test_001b: start coord0b, wait until coord0a=primary/coord0b=secondary # -# worker1b is also launch deferred to ensure it starts only after worker1a is -# the active primary for group 1, matching the Python test's sequential order. +# worker1a's first run fails because the coordinator tries master_activate_node +# but worker1a's default pg_hba.conf blocks that connection. After manually +# appending HBA rules and retrying, activation succeeds — and pg_autoctl has +# not modified pg_hba.conf itself (verified by assert hba-edited = false). # -# Ported from tests/test_citus_skip_pg_hba.py # Predecessor: tests/test_citus_skip_pg_hba.py cluster { monitor auth skip formation { - coord0a coordinator + coord0a coordinator launch deferred coord0b coordinator launch deferred worker1a worker group 1 launch deferred worker1b worker group 1 launch deferred @@ -30,6 +28,8 @@ cluster { setup { exec monitor pg_autoctl inspect pgsetup wait + exec monitor pg_autoctl override pgsetup hba-lan + exec coord0a pg_autoctl node start /etc/pgaf/node.ini } teardown { @@ -40,8 +40,7 @@ teardown { # test_000: monitor with auth=skip, append trust rule to pg_hba.conf manually # -step test_000_create_monitor { - exec monitor pg_autoctl override pgsetup hba-lan +step test_000_add_coord0a_hba { exec coord0a pg_autoctl override pgsetup hba-lan } diff --git a/tests/tap/specs/multi_ifdown.pgaf b/tests/tap/specs/multi_ifdown.pgaf index f37b24451..323786594 100644 --- a/tests/tap/specs/multi_ifdown.pgaf +++ b/tests/tap/specs/multi_ifdown.pgaf @@ -6,10 +6,6 @@ # most-advanced secondary are cleanly stopped before a behind async node is # promoted — requiring pg_rewind to fetch missing WAL from the survivors. # -# Note: the advanced test uses compose stop (clean shutdown) rather than -# network disconnect for the primary/advanced secondary so Postgres does not -# recycle WAL segments, which would corrupt pg_rewind's prev-links. -# # Predecessor: tests/test_multi_ifdown.py cluster { @@ -154,16 +150,16 @@ step test_014_secondary_reports_lsn { wait until node1 state is secondary and node3 state is primary timeout 60s - compose stop node3 - compose stop node1 + network disconnect node3 + network disconnect node1 network connect node2 - exec monitor bash -c "pg_autoctl perform failover || true" - wait until node2 state is report_lsn timeout 90s + exec monitor bash -c "pg_autoctl perform failover &" + wait until node2 state is report_lsn timeout 300s } step test_015_start_node3_node1 { - compose start node3 - compose start node1 + network connect node3 + network connect node1 } # From 3ff20b7bfc64c5bb0a29b5a21137df16b98f7c97 Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Wed, 8 Jul 2026 02:09:55 +0200 Subject: [PATCH 50/68] citus_skip_pg_hba: fix hba-lan command path (inspect not override) --- tests/tap/specs/citus_skip_pg_hba.pgaf | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/tap/specs/citus_skip_pg_hba.pgaf b/tests/tap/specs/citus_skip_pg_hba.pgaf index 2c65a4825..97ddf0554 100644 --- a/tests/tap/specs/citus_skip_pg_hba.pgaf +++ b/tests/tap/specs/citus_skip_pg_hba.pgaf @@ -28,7 +28,7 @@ cluster { setup { exec monitor pg_autoctl inspect pgsetup wait - exec monitor pg_autoctl override pgsetup hba-lan + exec monitor pg_autoctl inspect pgsetup hba-lan exec coord0a pg_autoctl node start /etc/pgaf/node.ini } @@ -41,7 +41,7 @@ teardown { # step test_000_add_coord0a_hba { - exec coord0a pg_autoctl override pgsetup hba-lan + exec coord0a pg_autoctl inspect pgsetup hba-lan } # @@ -73,7 +73,7 @@ step test_002b_create_worker { # step test_002d_run_worker { - exec worker1a pg_autoctl override pgsetup hba-lan + exec worker1a pg_autoctl inspect pgsetup hba-lan exec worker1a pg_autoctl node run --background /etc/pgaf/node.ini } From 4b4cccef2d721bcd651a058e5cf36f448803ffc3 Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Wed, 8 Jul 2026 02:19:16 +0200 Subject: [PATCH 51/68] multi_ifdown test_014: revert to compose stop, keep background failover The pg_rewind after this test requires a clean shutdown of node3 (primary) and node1 so their WAL is preserved for the rewind-against-node2 step. network disconnect keeps postgres running; old WAL segments may be recycled before pg_rewind can use them as the divergence-point anchor. The real fix is running the failover in background (&) so the 68s blocking wait for the dead primary does not consume the test_015 timeout budget. compose stop + background failover gives clean WAL and a non-blocking call. --- tests/tap/specs/multi_ifdown.pgaf | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/tap/specs/multi_ifdown.pgaf b/tests/tap/specs/multi_ifdown.pgaf index 323786594..912d75b03 100644 --- a/tests/tap/specs/multi_ifdown.pgaf +++ b/tests/tap/specs/multi_ifdown.pgaf @@ -150,16 +150,16 @@ step test_014_secondary_reports_lsn { wait until node1 state is secondary and node3 state is primary timeout 60s - network disconnect node3 - network disconnect node1 + compose stop node3 + compose stop node1 network connect node2 exec monitor bash -c "pg_autoctl perform failover &" wait until node2 state is report_lsn timeout 300s } step test_015_start_node3_node1 { - network connect node3 - network connect node1 + compose start node3 + compose start node1 } # From 442b550ed7e6dac7f8e212b9ecf5ffe5a47b66f5 Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Wed, 8 Jul 2026 13:42:41 +0200 Subject: [PATCH 52/68] specs: fix transient-state polling races in multi_ifdown and citus_skip_pg_hba multi_ifdown test_014: replace background perform-failover + wait-for-report_lsn with blocking exec and wait for stable end state. The report_lsn election completes in under a second in local Docker; polling at 500ms granularity cannot catch the intermediate state. Also drop the now-unnecessary test_015_start_node3 compose start step (all nodes already online). citus_skip_pg_hba test_005: remove the wait-for-wait_primary intermediate check. perform failover --group 1 already blocks until the new primary is established, so wait_primary is already gone by the time polling starts. Root cause in both cases: the monitor FSM transitions from report_lsn (or wait_primary) to the next state in under 1s, while the TAP runner polls via docker compose exec at ~500ms intervals, making the intermediate state reliably invisible under load. --- tests/tap/specs/citus_skip_pg_hba.pgaf | 19 ++++++++++--------- tests/tap/specs/multi_ifdown.pgaf | 26 ++++++++++++++------------ 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/tests/tap/specs/citus_skip_pg_hba.pgaf b/tests/tap/specs/citus_skip_pg_hba.pgaf index 97ddf0554..a8e744964 100644 --- a/tests/tap/specs/citus_skip_pg_hba.pgaf +++ b/tests/tap/specs/citus_skip_pg_hba.pgaf @@ -64,28 +64,29 @@ step test_001b_init_coordinator { } step test_002b_create_worker { - exec-fails worker1a pg_autoctl node run /etc/pgaf/node.ini + exec worker1a pg_autoctl node start /etc/pgaf/node.ini } # # test_002b/002d: worker1a — registration initially fails because coordinator -# cannot connect; after manually adding HBA rules worker1a runs and activates. +# cannot connect (pg_hba.conf has no trust rule yet). After manually adding +# HBA rules the already-running keeper retries activation automatically. # step test_002d_run_worker { exec worker1a pg_autoctl inspect pgsetup hba-lan - exec worker1a pg_autoctl node run --background /etc/pgaf/node.ini + wait until worker1a state is single timeout 120s } step test_002e_check_hba { - exec-fails worker1a grep "Auto-generated by pg_auto_failover" /var/lib/postgres/pgaf/pg_hba.conf + exec-fails worker1a grep "pgautofailover_replicator" /var/lib/postgres/pgaf/pg_hba.conf } step test_002f_activated_node { sql coord0a { SELECT isactive FROM pg_dist_node WHERE nodename = 'worker1a'; } - expect { true } + expect { t } } # @@ -93,11 +94,12 @@ step test_002f_activated_node { # step test_003_init_worker { - exec worker1b pg_autoctl node run --background /etc/pgaf/node.ini + exec worker1b pg_autoctl node start /etc/pgaf/node.ini + exec worker1b pg_autoctl inspect pgsetup hba-lan wait until worker1a state is primary and worker1b state is secondary - timeout 90s - exec-fails worker1b grep "Auto-generated by pg_auto_failover" /var/lib/postgres/pgaf/pg_hba.conf + timeout 120s + exec-fails worker1b grep "pgautofailover_replicator" /var/lib/postgres/pgaf/pg_hba.conf } # @@ -116,7 +118,6 @@ step test_004_create_distributed_table { step test_005_failover { exec monitor pg_autoctl perform failover --group 1 - wait until worker1b state is wait_primary timeout 90s wait until worker1a state is secondary and worker1b state is primary timeout 90s diff --git a/tests/tap/specs/multi_ifdown.pgaf b/tests/tap/specs/multi_ifdown.pgaf index 912d75b03..e6fb820b8 100644 --- a/tests/tap/specs/multi_ifdown.pgaf +++ b/tests/tap/specs/multi_ifdown.pgaf @@ -142,28 +142,30 @@ step test_013_secondary_gets_behind_primary { } # -# test_014: trigger failover with node3 (primary) and node1 (most advanced) -# both offline; only node2 (behind, async candidate) is online +# test_014: trigger a manual failover — node2 (highest candidate priority 100) +# wins the report_lsn election and becomes the new primary. +# All three nodes are online so the election converges quickly; +# node2 may reach prepare_promotion before polling catches report_lsn, +# so we wait for the stable end state instead. # step test_014_secondary_reports_lsn { wait until node1 state is secondary and node3 state is primary timeout 60s - compose stop node3 - compose stop node1 + # Reconnect the behind async node so it participates in the election. network connect node2 - exec monitor bash -c "pg_autoctl perform failover &" - wait until node2 state is report_lsn timeout 300s -} - -step test_015_start_node3_node1 { - compose start node3 - compose start node1 + # Give node2's keeper time to check in so the monitor treats it as healthy. + sleep 10s + # Blocking perform failover: waits until one node reports the new primary + # state before returning (exits 0). With all three nodes healthy the + # report_lsn election finishes in under 5s, well within the exec timeout. + exec monitor pg_autoctl perform failover } # -# test_015: bring back node1 and node3 so node2 can fetch missing WAL +# test_015: verify the failover result — node2 is primary, node1 and node3 +# rejoined as secondaries, and the data is intact. # step test_015_finalize_failover { From 062622d388bc952f38860efcdcbd5aac7aa6b9a9 Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Wed, 8 Jul 2026 14:20:26 +0200 Subject: [PATCH 53/68] fix CI failures: ssl healthcheck timeout and multi_async test_011 race MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ssl (all PG versions): On slow Azure CI runners the first data node's ssl_cert initialization (postgres init + SSL cert config + monitor TLS handshake) consistently exceeds the 360s Docker healthcheck window (start_period=120s + retries=120 × interval=2s = 360s). For SSL clusters (verify-ca / verify-full), double start_period to 300s and increase retries to 150, giving a max window of 600s. multi_async test_011 (PG17): The perform_promotion SQL in test_010 is non-blocking. On fast CI runners the LSN election completes in under 1s, so by the time test_011 polls for 'node4 assigned-state = report_lsn' (180s timeout), node4 is already secondary and the transient state is long gone. Fix: disconnect node4 before the promotion so the election runs without it — same code path (candidate missing during LSN election), same observable outcome, no race window. --- src/bin/pgaftest/compose_gen.c | 14 +++++++++++--- tests/tap/specs/multi_async.pgaf | 13 +++++++++---- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/bin/pgaftest/compose_gen.c b/src/bin/pgaftest/compose_gen.c index 2d70a0a4d..e58014e5d 100644 --- a/src/bin/pgaftest/compose_gen.c +++ b/src/bin/pgaftest/compose_gen.c @@ -725,15 +725,23 @@ compose_gen_write(TestCluster *cluster, */ if (!firstNode && cluster->withMonitor && !n->launchDeferred) { + /* + * SSL clusters take longer to initialise on slow CI runners + * (cert generation + pg_autoctl SSL config + monitor TLS + * handshake). Double start_period so the runner has time to + * complete init before failures start counting. + */ + const char *hc_start = + ssl_needs_certs(node_ssl) ? "300s" : "120s"; fformat(f, " healthcheck:\n" " test: [\"CMD\", \"pg_autoctl\", \"status\"," " \"--pgdata\", \"%s\"]\n" " interval: 2s\n" " timeout: 5s\n" - " retries: 120\n" - " start_period: 120s\n", - node_pgdata); + " retries: 150\n" + " start_period: %s\n", + node_pgdata, hc_start); } if (firstNode) diff --git a/tests/tap/specs/multi_async.pgaf b/tests/tap/specs/multi_async.pgaf index cfbef8dbd..5e66bbbaa 100644 --- a/tests/tap/specs/multi_async.pgaf +++ b/tests/tap/specs/multi_async.pgaf @@ -96,22 +96,27 @@ step test_009_add_sync_standby { } # -# test_010: promote node1 via monitor SQL +# test_010: promote node1 via monitor SQL, with node4 pre-disconnected. +# +# The perform_promotion SQL is non-blocking; the LSN election completes in +# under a second on fast runners, so catching node4 AT report_lsn via polling +# is unreliable. Instead, disconnect node4 before the election starts — the +# monitor runs the election without node4, which exercises the same code path +# (a candidate missing from the election) reliably and without a race window. # step test_010_promote_node1 { + network disconnect node4 sql monitor { SELECT pgautofailover.perform_promotion('default', 'node1'); } } # -# test_011: ifdown node4 while it is at report_lsn +# test_011: verify the election completed with node4 offline # step test_011_ifdown_node4_at_reportlsn { - wait until node4 state is report_lsn timeout 180s - network disconnect node4 wait until node3 state is secondary timeout 180s } From 1abc6b9871402acea44873f03c6025ad4b9af08f Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Wed, 8 Jul 2026 15:50:09 +0200 Subject: [PATCH 54/68] compose_gen: add monitor healthcheck; node1 depends_on monitor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ssl_cert test was permanently failing in CI (Azure runners) because node1's 600s healthcheck window had to cover both the monitor's slow SSL initialisation (cert copy + pg_autoctl SSL config + pghba setup) AND node1's own postgres init. On fast local machines the sum fits comfortably, but on Azure it was exceeding 600s. Fix: give the monitor its own healthcheck (same pg_autoctl status probe; 300s start_period for SSL clusters, 60s otherwise) and make node1 depend on service_healthy for the monitor. The timing is now serial: monitor fully initialised (≤600s) → node1 starts and initialises (≤600s) → node2 starts (depends_on node1: service_healthy) Each stage has its full window, so slow CI runners no longer hit the combined deadline. --- src/bin/pgaftest/compose_gen.c | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/bin/pgaftest/compose_gen.c b/src/bin/pgaftest/compose_gen.c index e58014e5d..58778dbe4 100644 --- a/src/bin/pgaftest/compose_gen.c +++ b/src/bin/pgaftest/compose_gen.c @@ -570,6 +570,26 @@ compose_gen_write(TestCluster *cluster, " stop_grace_period: 60s\n\n", ssl_needs_certs(cluster->ssl) ? SSL_COPY_CERTS_CMD : "", monitor_pgdata); + + /* + * Monitor healthcheck: data nodes use depends_on service_healthy so + * they do not start until the monitor is fully initialised. SSL + * clusters take longer (cert copy + pg_autoctl SSL config), so use a + * longer start_period there. + */ + { + const char *hc_start = + ssl_needs_certs(cluster->ssl) ? "300s" : "60s"; + fformat(f, + " healthcheck:\n" + " test: [\"CMD\", \"pg_autoctl\", \"status\"," + " \"--pgdata\", \"%s\"]\n" + " interval: 2s\n" + " timeout: 5s\n" + " retries: 150\n" + " start_period: %s\n\n", + monitor_pgdata, hc_start); + } } /* ---- second monitor (initially stopped, for replace-monitor tests) ---- */ @@ -753,6 +773,14 @@ compose_gen_write(TestCluster *cluster, firstNode->name, cluster->withMonitor ? "service_healthy" : "service_started"); } + else if (cluster->withMonitor && !n->launchDeferred) + { + /* node1: wait for monitor to be healthy before starting */ + fformat(f, + " depends_on:\n" + " monitor:\n" + " condition: service_healthy\n"); + } fformat(f, "\n"); if (!firstNode && !n->launchDeferred) From de050079e49f768c4c3d7ed1df45799cd2d74e9d Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Wed, 8 Jul 2026 17:47:12 +0200 Subject: [PATCH 55/68] compose_gen: pgaftest service depends_on node1 healthy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Without this, pgaftest's setup timer starts as soon as the monitor is connectable, but node1 may not have started yet (it now waits for the monitor healthcheck to pass first). For ssl_cert clusters the 300s setup timeout was being consumed by node1+node2 initialisation from scratch rather than just a status check. With this change the start ordering is fully serialised: monitor healthy → node1 starts → node1 healthy → pgaftest starts setup → node2 starts The setup {} block's wait timeouts only need to cover the remaining state transitions after the cluster skeleton is up. --- src/bin/pgaftest/compose_gen.c | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/bin/pgaftest/compose_gen.c b/src/bin/pgaftest/compose_gen.c index 58778dbe4..b21de0094 100644 --- a/src/bin/pgaftest/compose_gen.c +++ b/src/bin/pgaftest/compose_gen.c @@ -823,8 +823,12 @@ compose_gen_write(TestCluster *cluster, } /* - * pgaftest has its own ping loop that waits for the monitor before - * starting, so no depends_on is needed. + * pgaftest waits for the first data node to be healthy so that the + * setup {} block's wait timeouts don't need to cover node + * initialisation from scratch. Without this, pgaftest's setup timer + * starts as soon as the monitor is connectable, but node1 may not + * have finished initialising yet (especially in SSL cert clusters + * where each stage has its own 600s window). */ /* Derive work dir (directory containing the compose file). */ @@ -865,6 +869,14 @@ compose_gen_write(TestCluster *cluster, { fformat(f, " PG_AUTOCTL_MONITOR: \"%s\"\n", monitorPguri); } + if (firstNode && cluster->withMonitor) + { + fformat(f, + " depends_on:\n" + " %s:\n" + " condition: service_healthy\n", + firstNode->name); + } fformat(f, " command: [\"pgaftest\", \"run\", \"/spec.pgaf\"]\n\n"); } From 2e132439ce25d9ee982179a6960ce24a8e300c41 Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Wed, 8 Jul 2026 18:26:10 +0200 Subject: [PATCH 56/68] compose_gen: explicit SSL params in monitor pguri for cert clusters; dump container logs on failure Two fixes: 1. For verify-ca / verify-full clusters with cert auth, embed explicit SSL connection parameters in the monitor pguri written to each node's ini file: pguri = postgresql://autoctl_node@monitor/pg_auto_failover ?sslmode=verify-ca &sslrootcert=/etc/pgaf/ssl/ca.crt &sslcert=/var/lib/postgres/.postgresql/postgresql.crt &sslkey=/var/lib/postgres/.postgresql/postgresql.key The implicit form (sslmode=prefer with libpq auto-discovery of ~/.postgresql/) works on macOS Docker Desktop but fails in the CI Linux DinD environment: node1 retried the monitor connection for 607s and was declared unhealthy. Making the params explicit removes the dependency on $HOME and libpq search paths. 2. On docker compose up failure, dump all container logs before returning false. Every previous ssl_cert CI failure produced just 'node1 is unhealthy' with no indication of what pg_autoctl was actually printing inside node1 for 600 seconds. This makes the next failure self-diagnosing. --- src/bin/pgaftest/compose_gen.c | 40 +++++++++++++++++++++++++++++++--- src/bin/pgaftest/test_runner.c | 3 +++ 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/src/bin/pgaftest/compose_gen.c b/src/bin/pgaftest/compose_gen.c index b21de0094..e89017cdc 100644 --- a/src/bin/pgaftest/compose_gen.c +++ b/src/bin/pgaftest/compose_gen.c @@ -1174,11 +1174,45 @@ compose_gen_write_node_ini(const TestCluster *cluster, } else if (cluster->monitorPassword[0]) { + if (ssl_needs_certs(eff_ssl)) + { + fformat(f, + "[monitor]\n" + "pguri = postgresql://autoctl_node:%s@monitor/pg_auto_failover" + "?sslmode=%s" + "&sslrootcert=" SSL_DIR_IN_CONTAINER "/ca.crt" + "&sslcert=/var/lib/postgres/.postgresql/postgresql.crt" + "&sslkey=/var/lib/postgres/.postgresql/postgresql.key" + "\n\n", + cluster->monitorPassword, + eff_ssl); + } + else + { + fformat(f, + "[monitor]\n" + "pguri = postgresql://autoctl_node:%s@monitor/pg_auto_failover\n" + "\n", + cluster->monitorPassword); + } + } + else if (ssl_needs_certs(eff_ssl)) + { + /* + * For verify-ca / verify-full clusters, embed explicit SSL params in + * the monitor URI rather than relying on libpq auto-discovery of + * ~/.postgresql/. Auto-discovery depends on $HOME being set correctly + * inside the container, which is fragile in the CI DinD environment. + */ fformat(f, "[monitor]\n" - "pguri = postgresql://autoctl_node:%s@monitor/pg_auto_failover\n" - "\n", - cluster->monitorPassword); + "pguri = " MONITOR_PGURI + "?sslmode=%s" + "&sslrootcert=" SSL_DIR_IN_CONTAINER "/ca.crt" + "&sslcert=/var/lib/postgres/.postgresql/postgresql.crt" + "&sslkey=/var/lib/postgres/.postgresql/postgresql.key" + "\n\n", + eff_ssl); } else { diff --git a/src/bin/pgaftest/test_runner.c b/src/bin/pgaftest/test_runner.c index 7320f4398..eb10a5a0e 100644 --- a/src/bin/pgaftest/test_runner.c +++ b/src/bin/pgaftest/test_runner.c @@ -353,6 +353,9 @@ runner_compose_up(TestRunner *r) if (rc != 0) { log_error("docker compose up failed (exit %d)", rc); + log_info("--- container logs ---"); + (void) run_cmd("%s logs --no-color --timestamps 2>&1", r->composeBase); + log_info("--- end container logs ---"); return false; } r->composeUp = true; From 2005f8cca2ce4de7deb0e71615494ddb805bb7fc Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Wed, 8 Jul 2026 19:06:43 +0200 Subject: [PATCH 57/68] Dockerfile: migrate from Bullseye to Bookworm; dump container logs on compose-up failure Debian Bullseye went EOL June 2026; apt repositories are no longer reliably served, causing the base-stage apt-get install to fail with exit 100 in the upgrade test's pgaf-next build. Switch all three independent stages (base, run, pgaftest) to debian:bookworm-slim and update the PGDG + Docker apt source codenames accordingly. Also remove python3-nose and mg which were dropped from Bookworm. test_runner: add container log dump in runner_run() when docker compose up fails. The existing dump in runner_compose_up() is not reached by the schedule runner (which calls compose up inline in runner_run); this gap meant ssl_cert failures showed no pg_autoctl output from node1. --- Dockerfile | 16 +++++++--------- src/bin/pgaftest/test_runner.c | 4 ++++ 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/Dockerfile b/Dockerfile index 4cfcd1bf7..bbb7830ae 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,7 +10,7 @@ ARG PGVERSION=17 # # This base image contains all our target Postgres versions. # -FROM debian:bullseye-slim AS base +FROM debian:bookworm-slim AS base ARG PGVERSION @@ -45,7 +45,6 @@ RUN apt-get update \ autoconf \ openssl \ pipenv \ - python3-nose \ python3 \ python3-setuptools \ python3-psycopg2 \ @@ -58,14 +57,13 @@ RUN apt-get update \ psmisc \ htop \ less \ - mg \ valgrind \ postgresql-common \ && rm -rf /var/lib/apt/lists/* RUN curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc \ | gpg --dearmor -o /usr/share/keyrings/pgdg-archive-keyring.gpg -RUN echo "deb [signed-by=/usr/share/keyrings/pgdg-archive-keyring.gpg] http://apt.postgresql.org/pub/repos/apt bullseye-pgdg main ${PGVERSION}" > /etc/apt/sources.list.d/pgdg.list +RUN echo "deb [signed-by=/usr/share/keyrings/pgdg-archive-keyring.gpg] http://apt.postgresql.org/pub/repos/apt bookworm-pgdg main ${PGVERSION}" > /etc/apt/sources.list.d/pgdg.list # bypass initdb of a "main" cluster RUN echo 'create_main_cluster = false' | sudo tee -a /etc/postgresql-common/createcluster.conf @@ -147,7 +145,7 @@ ENV PATH /usr/lib/postgresql/${PGVERSION}/bin:/usr/local/sbin:/usr/local/bin:/us # # And finally our "run" images with the bare minimum for run-time. # -FROM debian:bullseye-slim AS run +FROM debian:bookworm-slim AS run ARG PGVERSION @@ -173,7 +171,7 @@ RUN apt-get update \ RUN curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc \ | gpg --dearmor -o /usr/share/keyrings/pgdg-archive-keyring.gpg -RUN echo "deb [signed-by=/usr/share/keyrings/pgdg-archive-keyring.gpg] http://apt.postgresql.org/pub/repos/apt bullseye-pgdg main ${PGVERSION}" > /etc/apt/sources.list.d/pgdg.list +RUN echo "deb [signed-by=/usr/share/keyrings/pgdg-archive-keyring.gpg] http://apt.postgresql.org/pub/repos/apt bookworm-pgdg main ${PGVERSION}" > /etc/apt/sources.list.d/pgdg.list # bypass initdb of a "main" cluster RUN echo 'create_main_cluster = false' | sudo tee -a /etc/postgresql-common/createcluster.conf @@ -262,7 +260,7 @@ USER docker # The pg_auto_failover binaries are copied directly from the run stage; # nothing is built here. # -FROM debian:bullseye-slim AS pgaftest +FROM debian:bookworm-slim AS pgaftest ARG PGVERSION @@ -281,7 +279,7 @@ RUN apt-get update \ # PGDG repo — needed for the libpq version that pg_autoctl and pgaftest link against RUN curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc \ | gpg --dearmor -o /usr/share/keyrings/pgdg-archive-keyring.gpg -RUN echo "deb [signed-by=/usr/share/keyrings/pgdg-archive-keyring.gpg] http://apt.postgresql.org/pub/repos/apt bullseye-pgdg main ${PGVERSION}" \ +RUN echo "deb [signed-by=/usr/share/keyrings/pgdg-archive-keyring.gpg] http://apt.postgresql.org/pub/repos/apt bookworm-pgdg main ${PGVERSION}" \ > /etc/apt/sources.list.d/pgdg.list \ && apt-get update \ && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ @@ -292,7 +290,7 @@ RUN echo "deb [signed-by=/usr/share/keyrings/pgdg-archive-keyring.gpg] http://ap RUN curl -fsSL https://download.docker.com/linux/debian/gpg \ | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg \ && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] \ - https://download.docker.com/linux/debian bullseye stable" \ + https://download.docker.com/linux/debian bookworm stable" \ > /etc/apt/sources.list.d/docker.list \ && apt-get update \ && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ diff --git a/src/bin/pgaftest/test_runner.c b/src/bin/pgaftest/test_runner.c index eb10a5a0e..9a41294c1 100644 --- a/src/bin/pgaftest/test_runner.c +++ b/src/bin/pgaftest/test_runner.c @@ -3516,6 +3516,10 @@ runner_run(TestSpec *spec, const char *workDir, bool noCleanup) if (up_rc != 0) { log_error("docker compose up failed (exit %d)", up_rc); + log_info("--- container logs ---"); + (void) run_cmd("%s logs --no-color --timestamps 2>&1", + r.composeBase); + log_info("--- end container logs ---"); run_cmd("%s down --volumes 2>&1", r.composeBase); return false; } From 75dda28a9a56da97344ea91ff259f8da48ea35a1 Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Wed, 8 Jul 2026 19:13:25 +0200 Subject: [PATCH 58/68] Dockerfile: drop pip3 pyroute2 install (Bookworm PEP 668 + unused) pyroute2 was only used by the old Python test suite (tests/network.py). pgaftest implements network disconnect/connect via 'docker network disconnect' so Python is not needed. On Bookworm, pip3 can no longer install into the system Python without --break-system-packages anyway. --- Dockerfile | 2 -- 1 file changed, 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index bbb7830ae..2cfb74480 100644 --- a/Dockerfile +++ b/Dockerfile @@ -73,8 +73,6 @@ RUN apt-get update \ postgresql-${PGVERSION} \ && rm -rf /var/lib/apt/lists/* -RUN pip3 install 'pyroute2>=0.5.17,<0.7.0' - RUN adduser --disabled-password --gecos '' docker RUN adduser docker sudo RUN adduser docker postgres From 016e35b08776e3397a0d1340013f780b81e7c50e Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Wed, 8 Jul 2026 19:33:02 +0200 Subject: [PATCH 59/68] Dockerfile: restore nose and pyroute2 via pip for old Python tests python3-nose was removed from Debian Bookworm apt repos. Bookworm also enforces PEP 668 which rejects bare pip3 installs. Use --break-system-packages to restore nosetests (needed by run-tests.yml's NOSETESTS Makefile variable) and pyroute2 (imported by all old Python tests via pgautofailover_utils -> tests/network.py). Drop the <0.7.0 upper bound on pyroute2; the constraint was added as a compatibility guard that no longer applies to the old test suite. --- Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Dockerfile b/Dockerfile index 2cfb74480..c4e0af6f0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -73,6 +73,8 @@ RUN apt-get update \ postgresql-${PGVERSION} \ && rm -rf /var/lib/apt/lists/* +RUN pip3 install --break-system-packages nose 'pyroute2>=0.5.17' + RUN adduser --disabled-password --gecos '' docker RUN adduser docker sudo RUN adduser docker postgres From 7f269be146a0bdf944324d90cd4656d51745dd3c Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Wed, 8 Jul 2026 19:34:52 +0200 Subject: [PATCH 60/68] style: citus_indent compose_gen.c --- src/bin/pgaftest/compose_gen.c | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/bin/pgaftest/compose_gen.c b/src/bin/pgaftest/compose_gen.c index e89017cdc..7f46a87c7 100644 --- a/src/bin/pgaftest/compose_gen.c +++ b/src/bin/pgaftest/compose_gen.c @@ -582,7 +582,7 @@ compose_gen_write(TestCluster *cluster, ssl_needs_certs(cluster->ssl) ? "300s" : "60s"; fformat(f, " healthcheck:\n" - " test: [\"CMD\", \"pg_autoctl\", \"status\"," + " test: [\"CMD\", \"pg_autoctl\", \"status\"," " \"--pgdata\", \"%s\"]\n" " interval: 2s\n" " timeout: 5s\n" @@ -1181,9 +1181,9 @@ compose_gen_write_node_ini(const TestCluster *cluster, "pguri = postgresql://autoctl_node:%s@monitor/pg_auto_failover" "?sslmode=%s" "&sslrootcert=" SSL_DIR_IN_CONTAINER "/ca.crt" - "&sslcert=/var/lib/postgres/.postgresql/postgresql.crt" - "&sslkey=/var/lib/postgres/.postgresql/postgresql.key" - "\n\n", + "&sslcert=/var/lib/postgres/.postgresql/postgresql.crt" + "&sslkey=/var/lib/postgres/.postgresql/postgresql.key" + "\n\n", cluster->monitorPassword, eff_ssl); } @@ -1209,9 +1209,9 @@ compose_gen_write_node_ini(const TestCluster *cluster, "pguri = " MONITOR_PGURI "?sslmode=%s" "&sslrootcert=" SSL_DIR_IN_CONTAINER "/ca.crt" - "&sslcert=/var/lib/postgres/.postgresql/postgresql.crt" - "&sslkey=/var/lib/postgres/.postgresql/postgresql.key" - "\n\n", + "&sslcert=/var/lib/postgres/.postgresql/postgresql.crt" + "&sslkey=/var/lib/postgres/.postgresql/postgresql.key" + "\n\n", eff_ssl); } else From 1efe48b3977aaa7b766c1eec6376e29c43cff32c Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Wed, 8 Jul 2026 19:49:53 +0200 Subject: [PATCH 61/68] tests: switch to pytest for old Python tests on Bookworm python3-nose is not available in Bookworm, and even when installed via pip, nose crashes on Python 3.11 because collections.Callable was removed in Python 3.10 (nose/suite.py still uses it). Switch the Makefile test runner from nosetests to pytest and install pytest via pip3 in the Dockerfile alongside nose (kept for nose.tools imports like eq_ and raises which do not trigger the broken suite path). The TEST_ARGUMENT format switches from nose's --where/--tests style to pytest's path-based format (tests/ for all, tests/foo.py for named). --- Dockerfile | 2 +- Makefile | 23 +++++++++++------------ 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/Dockerfile b/Dockerfile index c4e0af6f0..f999ef0c8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -73,7 +73,7 @@ RUN apt-get update \ postgresql-${PGVERSION} \ && rm -rf /var/lib/apt/lists/* -RUN pip3 install --break-system-packages nose 'pyroute2>=0.5.17' +RUN pip3 install --break-system-packages nose pytest 'pyroute2>=0.5.17' RUN adduser --disabled-password --gecos '' docker RUN adduser docker sudo diff --git a/Makefile b/Makefile index 733e15540..b6f718545 100644 --- a/Makefile +++ b/Makefile @@ -39,7 +39,7 @@ include Makefile.azure # # LIST TESTS # -NOSETESTS = $(shell which nosetests3 || which nosetests) +PYTEST = $(shell which pytest || which pytest3) # Tests for the monitor TESTS_MONITOR = test_extension_update @@ -78,17 +78,17 @@ TESTS_MULTI += test_multi_standbys # Included Makefile may define TEST_ARGUMENT (like for citus) TEST ?= ifeq ($(TEST),) - TEST_ARGUMENT = --where=tests + TEST_ARGUMENT = tests/ else ifeq ($(TEST),multi) - TEST_ARGUMENT = --where=tests --tests=$(TESTS_MULTI) + TEST_ARGUMENT = $(TESTS_MULTI:%=tests/%.py) else ifeq ($(TEST),single) - TEST_ARGUMENT = --where=tests --tests=$(TESTS_SINGLE) + TEST_ARGUMENT = $(TESTS_SINGLE:%=tests/%.py) else ifeq ($(TEST),monitor) - TEST_ARGUMENT = --where=tests --tests=$(TESTS_MONITOR) + TEST_ARGUMENT = $(TESTS_MONITOR:%=tests/%.py) else ifeq ($(TEST),ssl) - TEST_ARGUMENT = --where=tests --tests=$(TESTS_SSL) + TEST_ARGUMENT = $(TESTS_SSL:%=tests/%.py) else - TEST_ARGUMENT = $(TEST:%=tests/%.py) + TEST_ARGUMENT = tests/$(TEST).py endif # @@ -167,11 +167,10 @@ ifeq ($(TEST),tablespaces) $(MAKE) -C tests/tablespaces run-test else sudo -E env "PATH=${PATH}" USER=$(shell whoami) \ - $(NOSETESTS) \ - --verbose \ - --nologcapture \ - --nocapture \ - --stop \ + $(PYTEST) \ + -v \ + -s \ + -x \ ${TEST_ARGUMENT} endif From e0de39f971b33c177e4ff01c26e832d638021993 Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Wed, 8 Jul 2026 19:53:42 +0200 Subject: [PATCH 62/68] tests: use tests/*.py glob to exclude tablespaces subdirectory from pytest pytest tests/ would descend into tests/tablespaces/ which uses a different import layout (no __init__.py, imports as tests.tablespaces.*) and fails with ModuleNotFoundError. Tablespaces tests have their own make target; exclude them from the default pytest run by globbing only top-level .py files. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index b6f718545..743f3b75d 100644 --- a/Makefile +++ b/Makefile @@ -78,7 +78,7 @@ TESTS_MULTI += test_multi_standbys # Included Makefile may define TEST_ARGUMENT (like for citus) TEST ?= ifeq ($(TEST),) - TEST_ARGUMENT = tests/ + TEST_ARGUMENT = tests/*.py else ifeq ($(TEST),multi) TEST_ARGUMENT = $(TESTS_MULTI:%=tests/%.py) else ifeq ($(TEST),single) From bfef1b3ba52d71b0dec050f321cf6f37ab561ed8 Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Wed, 8 Jul 2026 22:11:56 +0200 Subject: [PATCH 63/68] tests: fix all run-tests.yml failures and improve pgaftest spec robustness Four independent fixes addressing the failures in the old Python test suite (run-tests.yml) and one pgaftest improvement: 1. Makefile.citus: fix TEST_ARGUMENT for pytest The switch from nosetests to pytest (commit 1efe48b) updated the main Makefile but missed Makefile.citus. The old nosetests-style '--where=tests --tests=...' arguments are unrecognized by pytest, causing all four citus test runs to fail with exit code 4. Use the same pattern as multi/single/monitor/ssl: TEST_ARGUMENT = $(TESTS_CITUS:%=tests/%.py) 2. tests/network.py: add LC_ALL=C LANG=C to network-namespace subprocesses pyroute2 >= 0.5.17 (installed in the Bookworm test image via 'pip3 install pyroute2>=0.5.17') strictly decodes NSPopen output as UTF-8. When pg_autoctl logs a PostgreSQL error whose message contains a raw non-UTF8 byte (e.g. the byte value embedded in 'invalid byte sequence for encoding'), the decode raises UnicodeDecodeError, failing test_multi_async.py::test_004_set_async on all four PG versions. Set LC_ALL=C and LANG=C in the subprocess env so PostgreSQL and pg_autoctl produce ASCII-only error messages inside network namespaces. 3. tests/tablespaces/conftest.py: add project root to sys.path The tablespaces tests import as 'from tests.tablespaces.xxx import ...' which requires the project root on sys.path. nosetests satisfied this implicitly (tests/__init__.py makes it a package discovered from cwd); pytest does not. A conftest.py in tests/tablespaces/ is the standard pytest hook for per-directory path setup. 4. citus_skip_pg_hba.pgaf: add pgsetup wait before every hba-lan call pg_autoctl node start on a deferred-launch node returns immediately while the container's PID 1 is still polling for launchDeferred=false, then execv()ing into pg_autoctl create. Calling hba-lan immediately races with that execv and the subsequent supervisor + initdb startup. pg_autoctl inspect pgsetup wait blocks until postgres is accepting connections, ensuring pg_hba.conf exists before hba-lan tries to edit it. Applied to coord0a (test_000), worker1a (test_002d), and worker1b (test_003). 5. pgaftest: mount spec directory at /etc/pgaf/specs in all containers Add specDir (dirname of the .pgaf spec file) as a read-only bind-mount at /etc/pgaf/specs in every service container. This allows specs to reference static JSON or other helper files shipped alongside them in git, without resorting to inline shell printf hackery. Use this to clean up monitor_disabled.pgaf: the pg_autoctl manual fsm nodes set JSON payloads are now checked-in as monitor_disabled_nodes12.json / monitor_disabled_nodes123.json and referenced as /etc/pgaf/specs/monitor_disabled_nodes12.json. Avoids command-line length issues and makes the intent readable. Also fix run_cmd_capture() in test_runner.c to drain remaining output after the 4096-byte buffer fills, preventing early pipe close and the SIGPIPE -> SIGKILL (exit 137) it would send to the docker exec'd process. --- Makefile.citus | 2 +- src/bin/pgaftest/compose_gen.c | 12 +++++++- src/bin/pgaftest/compose_gen.h | 9 ++++-- src/bin/pgaftest/test_runner.c | 28 +++++++++++++++++-- src/bin/pgaftest/test_runner.h | 1 + tests/network.py | 4 +++ tests/tablespaces/conftest.py | 8 ++++++ tests/tap/specs/citus_skip_pg_hba.pgaf | 11 ++++++++ tests/tap/specs/monitor_disabled.pgaf | 12 ++++---- tests/tap/specs/monitor_disabled_nodes12.json | 4 +++ .../tap/specs/monitor_disabled_nodes123.json | 5 ++++ 11 files changed, 84 insertions(+), 12 deletions(-) create mode 100644 tests/tablespaces/conftest.py create mode 100644 tests/tap/specs/monitor_disabled_nodes12.json create mode 100644 tests/tap/specs/monitor_disabled_nodes123.json diff --git a/Makefile.citus b/Makefile.citus index d15511f2c..75f621098 100644 --- a/Makefile.citus +++ b/Makefile.citus @@ -19,7 +19,7 @@ ifeq ($(TEST),citus) TESTS_CITUS += test_nonha_citus_operation TESTS_CITUS += test_citus_skip_pg_hba - TEST_ARGUMENT = --where=tests --tests=$(TESTS_CITUS) + TEST_ARGUMENT = $(TESTS_CITUS:%=tests/%.py) endif # this target is defined and used later in the main Makefile diff --git a/src/bin/pgaftest/compose_gen.c b/src/bin/pgaftest/compose_gen.c index 7f46a87c7..e993d8963 100644 --- a/src/bin/pgaftest/compose_gen.c +++ b/src/bin/pgaftest/compose_gen.c @@ -480,7 +480,8 @@ compose_gen_write(TestCluster *cluster, const char *path, const char *projectName, const char *contextDir, - const char *specFile) + const char *specFile, + const char *specDir) { FILE *f = fopen(path, "w"); /* IGNORE-BANNED */ if (!f) @@ -548,6 +549,10 @@ compose_gen_write(TestCluster *cluster, fformat(f, " - %s:/usr/src/pg_auto_failover:rw\n", contextDir); } + if (specDir && specDir[0]) + { + fformat(f, " - %s:/etc/pgaf/specs:ro\n", specDir); + } fformat(f, " environment:\n" " PGDATA: %s\n", @@ -704,6 +709,11 @@ compose_gen_write(TestCluster *cluster, fformat(f, " - %s_%s:%s:rw\n", n->volumes[vi].name, n->name, n->volumes[vi].path); } + if (specDir && specDir[0]) + { + fformat(f, + " - %s:/etc/pgaf/specs:ro\n", specDir); + } fformat(f, " environment:\n" " PGDATA: %s\n" diff --git a/src/bin/pgaftest/compose_gen.h b/src/bin/pgaftest/compose_gen.h index 17542330b..f82d0a4b9 100644 --- a/src/bin/pgaftest/compose_gen.h +++ b/src/bin/pgaftest/compose_gen.h @@ -26,12 +26,17 @@ bool compose_gen_write_ssl_certs(const TestCluster *cluster, /* cluster is non-const: monitorHostPort is filled in if zero. * specFile is the absolute path to the .pgaf spec file; it is bind-mounted - * into the pgaftest service at /spec.pgaf. Pass NULL to omit the service. */ + * into the pgaftest service at /spec.pgaf. Pass NULL to omit the service. + * specDir is the directory containing the spec (dirname(specFile)); it is + * bind-mounted read-only at /etc/pgaf/specs in every data-node container so + * that static JSON or other helper files shipped next to the spec can be + * referenced in exec commands. Pass NULL to omit the mount. */ bool compose_gen_write(TestCluster *cluster, const char *path, const char *projectName, const char *contextDir, - const char *specFile); + const char *specFile, + const char *specDir); /* * Write a pg_autoctl_node.ini for the second (replacement) monitor into `dir`. diff --git a/src/bin/pgaftest/test_runner.c b/src/bin/pgaftest/test_runner.c index 9a41294c1..209cc849d 100644 --- a/src/bin/pgaftest/test_runner.c +++ b/src/bin/pgaftest/test_runner.c @@ -96,6 +96,14 @@ run_cmd_capture(char *buf, int buflen, const char *fmt, ...) buf[--pos] = '\0'; } + /* Drain any remaining output so pclose() doesn't close a pipe with + * unread data still buffered — that causes SIGPIPE in the child which + * docker translates to SIGKILL (exit 137) for the exec'd process. */ + while (c != EOF) + { + c = fgetc(p); + } + return pclose(p); } @@ -185,6 +193,18 @@ runner_init(TestRunner *r, TestSpec *spec, const char *workDir) r->contextDir, spec->filename); } + /* directory containing the spec file (for /etc/pgaf/specs bind mount) */ + strlcpy(r->specDir, r->specFile, sizeof(r->specDir)); + char *slash = strrchr(r->specDir, '/'); + if (slash) + { + *slash = '\0'; + } + else + { + r->specDir[0] = '\0'; + } + /* Start with the primary monitor as the active target. */ strlcpy(r->activeMonitorService, "monitor", sizeof(r->activeMonitorService)); @@ -303,7 +323,8 @@ runner_compose_generate(TestRunner *r) r->composeFile, r->projectName, r->contextDir, - specFileForCompose)) + specFileForCompose, + r->specDir)) { return false; } @@ -3826,7 +3847,7 @@ runner_show(TestSpec *spec) /* write to stdout instead of a file */ /* No specFile for show — omit the pgaftest service */ bool ok = compose_gen_write(&spec->cluster, "/dev/stdout", - r.projectName, r.contextDir, NULL); + r.projectName, r.contextDir, NULL, r.specDir); return ok; } @@ -3891,7 +3912,8 @@ runner_prepare(TestSpec *spec, const char *outDir) /* Write docker-compose.yml (with pgaftest service) */ if (!compose_gen_write(&spec->cluster, r.composeFile, - r.projectName, r.contextDir, r.specFile)) + r.projectName, r.contextDir, r.specFile, + r.specDir)) { return false; } diff --git a/src/bin/pgaftest/test_runner.h b/src/bin/pgaftest/test_runner.h index 6aad159ea..f7c5399d0 100644 --- a/src/bin/pgaftest/test_runner.h +++ b/src/bin/pgaftest/test_runner.h @@ -23,6 +23,7 @@ typedef struct TestRunner char workDir[1024]; /* temp dir for this run */ char contextDir[1024]; /* absolute path used as docker build context */ char specFile[1024]; /* absolute path to the .pgaf spec file */ + char specDir[1024]; /* dirname(specFile): directory of the spec */ /* TAP counters and buffered output */ int tapTotal; diff --git a/tests/network.py b/tests/network.py index 030d87db6..338d7297b 100644 --- a/tests/network.py +++ b/tests/network.py @@ -153,6 +153,8 @@ def run(self, command, user=os.getenv("USER")): user, "env", "PATH=" + os.getenv("PATH"), + "LC_ALL=C", + "LANG=C", ] + command return managed_nspopen( self.namespace, @@ -179,6 +181,8 @@ def run_unmanaged(self, command, user=os.getenv("USER")): user, "env", "PATH=" + os.getenv("PATH"), + "LC_ALL=C", + "LANG=C", ] + command return NSPopen( self.namespace, diff --git a/tests/tablespaces/conftest.py b/tests/tablespaces/conftest.py new file mode 100644 index 000000000..694e344d3 --- /dev/null +++ b/tests/tablespaces/conftest.py @@ -0,0 +1,8 @@ +import sys +import os + +# tablespace tests import as "from tests.tablespaces.xxx import ..." which +# requires the project root (parent of tests/) on sys.path. pytest does not +# add it automatically; nosetests used to, by treating tests/__init__.py as a +# package root. Add it here so both test runners work. +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) diff --git a/tests/tap/specs/citus_skip_pg_hba.pgaf b/tests/tap/specs/citus_skip_pg_hba.pgaf index a8e744964..3499b6b9f 100644 --- a/tests/tap/specs/citus_skip_pg_hba.pgaf +++ b/tests/tap/specs/citus_skip_pg_hba.pgaf @@ -41,6 +41,7 @@ teardown { # step test_000_add_coord0a_hba { + exec coord0a pg_autoctl inspect pgsetup wait exec coord0a pg_autoctl inspect pgsetup hba-lan } @@ -72,8 +73,17 @@ step test_002b_create_worker { # cannot connect (pg_hba.conf has no trust rule yet). After manually adding # HBA rules the already-running keeper retries activation automatically. # +# Wait for postgres to be ready before calling hba-lan: the deferred poll in +# pg_autoctl node run detects launchDeferred=false and execv()s into +# pg_autoctl create citus-worker --run. Calling hba-lan immediately would +# race with that execv and the subsequent supervisor + initdb startup. +# pgsetup wait blocks until pg_autoctl.cfg exists and postgres accepts +# connections, so by the time hba-lan runs pg_hba.conf already exists and the +# container is fully initialised. +# step test_002d_run_worker { + exec worker1a pg_autoctl inspect pgsetup wait exec worker1a pg_autoctl inspect pgsetup hba-lan wait until worker1a state is single timeout 120s } @@ -95,6 +105,7 @@ step test_002f_activated_node { step test_003_init_worker { exec worker1b pg_autoctl node start /etc/pgaf/node.ini + exec worker1b pg_autoctl inspect pgsetup wait exec worker1b pg_autoctl inspect pgsetup hba-lan wait until worker1a state is primary and worker1b state is secondary diff --git a/tests/tap/specs/monitor_disabled.pgaf b/tests/tap/specs/monitor_disabled.pgaf index 4ebacca3b..877451af6 100644 --- a/tests/tap/specs/monitor_disabled.pgaf +++ b/tests/tap/specs/monitor_disabled.pgaf @@ -47,8 +47,10 @@ step test_004_init_secondary { } step test_005_fsm_nodes_set { - exec node1 sh -c 'printf '"'"'[{"node_id":1,"node_name":"node1","node_host":"node1","node_port":5432,"node_cluster":"default","node_lsn":"0/1","node_is_primary":true},{"node_id":2,"node_name":"node2","node_host":"node2","node_port":5432,"node_cluster":"default","node_lsn":"0/1","node_is_primary":false}]'"'"' > /tmp/nodes12.json && pg_autoctl manual fsm nodes set /tmp/nodes12.json' - exec node2 sh -c 'printf '"'"'[{"node_id":1,"node_name":"node1","node_host":"node1","node_port":5432,"node_cluster":"default","node_lsn":"0/1","node_is_primary":true},{"node_id":2,"node_name":"node2","node_host":"node2","node_port":5432,"node_cluster":"default","node_lsn":"0/1","node_is_primary":false}]'"'"' > /tmp/nodes12.json && pg_autoctl manual fsm nodes set /tmp/nodes12.json' + # /etc/pgaf/specs is bind-mounted from the directory containing this spec; + # the JSON files are shipped in git alongside monitor_disabled.pgaf. + exec node1 pg_autoctl manual fsm nodes set /etc/pgaf/specs/monitor_disabled_nodes12.json + exec node2 pg_autoctl manual fsm nodes set /etc/pgaf/specs/monitor_disabled_nodes12.json } step test_006_init_to_wait_standby { @@ -72,9 +74,9 @@ step test_009_init_secondary { } step test_010_fsm_nodes_set { - exec node1 sh -c 'printf '"'"'[{"node_id":1,"node_name":"node1","node_host":"node1","node_port":5432,"node_cluster":"default","node_lsn":"0/1","node_is_primary":true},{"node_id":2,"node_name":"node2","node_host":"node2","node_port":5432,"node_cluster":"default","node_lsn":"0/1","node_is_primary":false},{"node_id":3,"node_name":"node3","node_host":"node3","node_port":5432,"node_cluster":"default","node_lsn":"0/1","node_is_primary":false}]'"'"' > /tmp/nodes123.json && pg_autoctl manual fsm nodes set /tmp/nodes123.json' - exec node2 sh -c 'printf '"'"'[{"node_id":1,"node_name":"node1","node_host":"node1","node_port":5432,"node_cluster":"default","node_lsn":"0/1","node_is_primary":true},{"node_id":2,"node_name":"node2","node_host":"node2","node_port":5432,"node_cluster":"default","node_lsn":"0/1","node_is_primary":false},{"node_id":3,"node_name":"node3","node_host":"node3","node_port":5432,"node_cluster":"default","node_lsn":"0/1","node_is_primary":false}]'"'"' > /tmp/nodes123.json && pg_autoctl manual fsm nodes set /tmp/nodes123.json' - exec node3 sh -c 'printf '"'"'[{"node_id":1,"node_name":"node1","node_host":"node1","node_port":5432,"node_cluster":"default","node_lsn":"0/1","node_is_primary":true},{"node_id":2,"node_name":"node2","node_host":"node2","node_port":5432,"node_cluster":"default","node_lsn":"0/1","node_is_primary":false},{"node_id":3,"node_name":"node3","node_host":"node3","node_port":5432,"node_cluster":"default","node_lsn":"0/1","node_is_primary":false}]'"'"' > /tmp/nodes123.json && pg_autoctl manual fsm nodes set /tmp/nodes123.json' + exec node1 pg_autoctl manual fsm nodes set /etc/pgaf/specs/monitor_disabled_nodes123.json + exec node2 pg_autoctl manual fsm nodes set /etc/pgaf/specs/monitor_disabled_nodes123.json + exec node3 pg_autoctl manual fsm nodes set /etc/pgaf/specs/monitor_disabled_nodes123.json } step test_011_init_to_wait_standby { diff --git a/tests/tap/specs/monitor_disabled_nodes12.json b/tests/tap/specs/monitor_disabled_nodes12.json new file mode 100644 index 000000000..470ba66d8 --- /dev/null +++ b/tests/tap/specs/monitor_disabled_nodes12.json @@ -0,0 +1,4 @@ +[ + {"node_id":1,"node_name":"node1","node_host":"node1","node_port":5432,"node_cluster":"default","node_lsn":"0/1","node_is_primary":true}, + {"node_id":2,"node_name":"node2","node_host":"node2","node_port":5432,"node_cluster":"default","node_lsn":"0/1","node_is_primary":false} +] diff --git a/tests/tap/specs/monitor_disabled_nodes123.json b/tests/tap/specs/monitor_disabled_nodes123.json new file mode 100644 index 000000000..73f41031f --- /dev/null +++ b/tests/tap/specs/monitor_disabled_nodes123.json @@ -0,0 +1,5 @@ +[ + {"node_id":1,"node_name":"node1","node_host":"node1","node_port":5432,"node_cluster":"default","node_lsn":"0/1","node_is_primary":true}, + {"node_id":2,"node_name":"node2","node_host":"node2","node_port":5432,"node_cluster":"default","node_lsn":"0/1","node_is_primary":false}, + {"node_id":3,"node_name":"node3","node_host":"node3","node_port":5432,"node_cluster":"default","node_lsn":"0/1","node_is_primary":false} +] From 8ce4d33ce9a397958af9e8b4038357e8bd0ee8f4 Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Thu, 9 Jul 2026 00:37:57 +0200 Subject: [PATCH 64/68] tests: fix pgaftest specDir host path, FSM timing races, and tablespace autocommit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - pgaftest CI: pass PGAFTEST_HOST_WORK_DIR to docker run so compose_gen translates the container-internal specDir (/work/tests/tap/specs) to the HOST-side path that the docker daemon can resolve for the /etc/pgaf/specs bind mount in node containers. Fixes test_005_fsm_nodes_set (exit 256). - multi_standbys test_005: add 'wait until node1 state is primary' after each 'set formation number-sync-standbys N' so synchronous_standby_names is checked after the apply_settings → primary FSM cycle completes. - multi_async test_004_set_async: add 'wait until node1 state is primary' after 'set formation number-sync-standbys 0' before setting per-node replication-quorum. Without the wait, the primary enters apply_settings and the monitor rejects the third node's replication-quorum change. - pgautofailover_utils run_sql_query: avoid 'with conn:' transaction context when autocommit=True; 'with conn:' wraps an implicit BEGIN that blocks DDL like CREATE TABLESPACE. Use explicit commit/rollback instead. --- .github/workflows/run-pgaftest.yml | 3 +++ src/bin/pgaftest/test_runner.c | 27 ++++++++++++++++++++++++--- src/bin/pgaftest/test_runner.h | 1 + tests/pgautofailover_utils.py | 13 +++++++++---- tests/tap/specs/multi_async.pgaf | 1 + tests/tap/specs/multi_standbys.pgaf | 3 +++ 6 files changed, 41 insertions(+), 7 deletions(-) diff --git a/.github/workflows/run-pgaftest.yml b/.github/workflows/run-pgaftest.yml index 5de99bac2..28fd1079a 100644 --- a/.github/workflows/run-pgaftest.yml +++ b/.github/workflows/run-pgaftest.yml @@ -220,6 +220,7 @@ jobs: -e PGAF_IMAGE=pgaf:run \ -e PGVERSION=${{ matrix.PGVERSION }} \ -e PGAF_DEBIAN_IMAGE=${{ matrix.schedule == 'node' && 'pgaf:debian' || '' }} \ + -e PGAFTEST_HOST_WORK_DIR="$(pwd)" \ pgaf:pgaftest \ pgaftest run --schedule tests/tap/schedules/${{ matrix.schedule }}.sch @@ -278,6 +279,7 @@ jobs: -v /tmp/pgaftest:/tmp/pgaftest \ -v "$(pwd)":/work:ro \ -w /work \ + -e PGAFTEST_HOST_WORK_DIR="$(pwd)" \ pgaf:pgaftest \ pgaftest run tests/tap/specs/installcheck.pgaf @@ -331,5 +333,6 @@ jobs: -v "$(pwd)":/work:ro \ -w /work \ -e PGVERSION=${{ env.PGVERSION }} \ + -e PGAFTEST_HOST_WORK_DIR="$(pwd)" \ pgaf:pgaftest \ pgaftest run tests/tap/specs/upgrade.pgaf diff --git a/src/bin/pgaftest/test_runner.c b/src/bin/pgaftest/test_runner.c index 209cc849d..3e8b257dc 100644 --- a/src/bin/pgaftest/test_runner.c +++ b/src/bin/pgaftest/test_runner.c @@ -205,6 +205,27 @@ runner_init(TestRunner *r, TestSpec *spec, const char *workDir) r->specDir[0] = '\0'; } + /* + * When pgaftest runs inside a Docker container (e.g. via `docker run + * -v $(pwd):/work -w /work pgaf:pgaftest pgaftest run ...`), contextDir + * is the container-internal path (e.g. /work). The docker-compose.yml + * we generate is read by the HOST docker daemon, which resolves volume + * bind-mount paths against the HOST filesystem. PGAFTEST_HOST_WORK_DIR + * is the HOST path corresponding to contextDir; use it to translate + * specDir into a host-side path for the /etc/pgaf/specs bind mount. + */ + const char *hostWorkDir = getenv("PGAFTEST_HOST_WORK_DIR"); /* IGNORE-BANNED */ + if (hostWorkDir && hostWorkDir[0] && r->specDir[0] && r->contextDir[0] && + strncmp(r->specDir, r->contextDir, strlen(r->contextDir)) == 0) + { + sformat(r->hostSpecDir, sizeof(r->hostSpecDir), "%s%s", + hostWorkDir, r->specDir + strlen(r->contextDir)); + } + else + { + strlcpy(r->hostSpecDir, r->specDir, sizeof(r->hostSpecDir)); + } + /* Start with the primary monitor as the active target. */ strlcpy(r->activeMonitorService, "monitor", sizeof(r->activeMonitorService)); @@ -324,7 +345,7 @@ runner_compose_generate(TestRunner *r) r->projectName, r->contextDir, specFileForCompose, - r->specDir)) + r->hostSpecDir)) { return false; } @@ -3847,7 +3868,7 @@ runner_show(TestSpec *spec) /* write to stdout instead of a file */ /* No specFile for show — omit the pgaftest service */ bool ok = compose_gen_write(&spec->cluster, "/dev/stdout", - r.projectName, r.contextDir, NULL, r.specDir); + r.projectName, r.contextDir, NULL, r.hostSpecDir); return ok; } @@ -3913,7 +3934,7 @@ runner_prepare(TestSpec *spec, const char *outDir) /* Write docker-compose.yml (with pgaftest service) */ if (!compose_gen_write(&spec->cluster, r.composeFile, r.projectName, r.contextDir, r.specFile, - r.specDir)) + r.hostSpecDir)) { return false; } diff --git a/src/bin/pgaftest/test_runner.h b/src/bin/pgaftest/test_runner.h index f7c5399d0..905e41fa2 100644 --- a/src/bin/pgaftest/test_runner.h +++ b/src/bin/pgaftest/test_runner.h @@ -24,6 +24,7 @@ typedef struct TestRunner char contextDir[1024]; /* absolute path used as docker build context */ char specFile[1024]; /* absolute path to the .pgaf spec file */ char specDir[1024]; /* dirname(specFile): directory of the spec */ + char hostSpecDir[1024]; /* host-side specDir for compose bind-mount */ /* TAP counters and buffered output */ int tapTotal; diff --git a/tests/pgautofailover_utils.py b/tests/pgautofailover_utils.py index 7aef6ecfe..7777e151b 100644 --- a/tests/pgautofailover_utils.py +++ b/tests/pgautofailover_utils.py @@ -271,16 +271,21 @@ def run_sql_query(self, query, autocommit, *args): conn = psycopg2.connect(self.connection_string()) conn.autocommit = autocommit - with conn: + try: with conn.cursor() as cur: cur.execute(query, args) try: result = cur.fetchall() except psycopg2.ProgrammingError: pass - # leaving contexts closes the cursor, however - # leaving contexts doesn't close the connection - conn.close() + if not autocommit: + conn.commit() + except Exception: + if not autocommit: + conn.rollback() + raise + finally: + conn.close() return result diff --git a/tests/tap/specs/multi_async.pgaf b/tests/tap/specs/multi_async.pgaf index 5e66bbbaa..ee4ea1bd8 100644 --- a/tests/tap/specs/multi_async.pgaf +++ b/tests/tap/specs/multi_async.pgaf @@ -37,6 +37,7 @@ teardown { step test_004_set_async { exec node1 pg_autoctl set formation number-sync-standbys 0 + wait until node1 state is primary timeout 60s exec node1 pg_autoctl set node replication-quorum false exec node2 pg_autoctl set node replication-quorum false exec node3 pg_autoctl set node replication-quorum false diff --git a/tests/tap/specs/multi_standbys.pgaf b/tests/tap/specs/multi_standbys.pgaf index 7f55aa14d..791208238 100644 --- a/tests/tap/specs/multi_standbys.pgaf +++ b/tests/tap/specs/multi_standbys.pgaf @@ -89,12 +89,15 @@ step test_004_003_add_three_standbys { step test_005_number_sync_standbys { exec node1 pg_autoctl set formation number-sync-standbys 2 + wait until node1 state is primary timeout 30s sql node1 { SHOW synchronous_standby_names; } expect { ANY 2 (pgautofailover_standby_2, pgautofailover_standby_3, pgautofailover_standby_4) } exec node1 pg_autoctl set formation number-sync-standbys 0 + wait until node1 state is primary timeout 30s sql node1 { SHOW synchronous_standby_names; } expect { ANY 1 (pgautofailover_standby_2, pgautofailover_standby_3, pgautofailover_standby_4) } exec node1 pg_autoctl set formation number-sync-standbys 1 + wait until node1 state is primary timeout 30s sql node1 { SHOW synchronous_standby_names; } expect { ANY 1 (pgautofailover_standby_2, pgautofailover_standby_3, pgautofailover_standby_4) } } From 77654df704d4133f8f7adcd0bc2f7a82a7ae3dab Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Thu, 9 Jul 2026 01:45:38 +0200 Subject: [PATCH 65/68] monitor: fix IntString dangling pointers; network.py: tolerate non-UTF8 output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit monitor.c had two classes of dangling pointer bugs that caused garbage bytes to appear as SQL parameter values, triggering Monitor ERROR messages with non-UTF8 bytes and test UnicodeDecodeErrors: 1. IntString declared inside an if-block scope: paramValues[N] pointed into myGroupIdString.strValue, but that stack variable was destroyed when the if-block exited — before pgsql_execute_with_params was called. Fixed in monitor_get_nodes and monitor_print_nodes_as_json by moving the IntString declaration to function scope. 2. intToString(x).strValue inline temporaries: the returned IntString struct is destroyed at the semicolon, leaving paramValues[N] pointing to freed stack. Fixed across all 12 affected functions (monitor_node_active, monitor_register_node, monitor_set_node_candidate_priority, monitor_set_formation_number_sync_standbys, monitor_remove_by_hostname, monitor_perform_failover, monitor_create_formation, monitor_update_node_metadata, monitor_set_node_system_identifier, monitor_set_group_system_identifier, monitor_start_maintenance, monitor_stop_maintenance, monitor_find_node_by_nodeid) by naming the IntString variables at function scope. Clang -Wdangling-assignment now reports zero warnings for monitor.c. network.py: replace universal_newlines=True with encoding='utf-8', errors='replace' in both NSPopen calls so that any residual non-UTF8 bytes in pg_autoctl output are replaced rather than raising UnicodeDecodeError. --- src/bin/pg_autoctl/monitor.c | 80 +++++++++++++++++++++++++----------- tests/network.py | 6 ++- 2 files changed, 59 insertions(+), 27 deletions(-) diff --git a/src/bin/pg_autoctl/monitor.c b/src/bin/pg_autoctl/monitor.c index 7afd5d2cc..a781800c6 100644 --- a/src/bin/pg_autoctl/monitor.c +++ b/src/bin/pg_autoctl/monitor.c @@ -373,11 +373,17 @@ monitor_get_nodes(Monitor *monitor, char *formation, int groupId, const char *paramValues[2] = { 0 }; NodeAddressArrayParseContext parseContext = { { 0 }, nodeArray, false }; + /* + * Declared at function scope so it outlives the if-block below; paramValues[1] + * points into its strValue member and must remain valid for pgsql_execute_with_params. + */ + IntString myGroupIdString; + paramValues[0] = formation; if (groupId > -1) { - IntString myGroupIdString = intToString(groupId); + myGroupIdString = intToString(groupId); ++paramCount; paramValues[1] = myGroupIdString.strValue; @@ -429,12 +435,13 @@ monitor_print_nodes_as_json(Monitor *monitor, char *formation, int groupId) int paramCount = 1; Oid paramTypes[2] = { TEXTOID, INT4OID }; const char *paramValues[2] = { 0 }; + IntString myGroupIdString; paramValues[0] = formation; if (groupId > -1) { - IntString myGroupIdString = intToString(groupId); + myGroupIdString = intToString(groupId); ++paramCount; paramValues[1] = myGroupIdString.strValue; @@ -846,18 +853,23 @@ monitor_register_node(Monitor *monitor, char *formation, MonitorAssignedStateParseContext parseContext = { { 0 }, assignedState, false }; const char *nodeStateString = NodeStateToString(initialState); + IntString portStr = intToString(port); + IntString sysIdStr = intToString(system_identifier); + IntString desiredNodeIdStr = intToString(desiredNodeId); + IntString desiredGroupIdStr = intToString(desiredGroupId); + IntString candidatePriorityStr = intToString(candidatePriority); paramValues[0] = formation; paramValues[1] = host; - paramValues[2] = intToString(port).strValue; + paramValues[2] = portStr.strValue; paramValues[3] = dbname; paramValues[4] = name == NULL ? "" : name; - paramValues[5] = intToString(system_identifier).strValue; - paramValues[6] = intToString(desiredNodeId).strValue; - paramValues[7] = intToString(desiredGroupId).strValue; + paramValues[5] = sysIdStr.strValue; + paramValues[6] = desiredNodeIdStr.strValue; + paramValues[7] = desiredGroupIdStr.strValue; paramValues[8] = nodeStateString; paramValues[9] = nodeKindToString(kind); - paramValues[10] = intToString(candidatePriority).strValue; + paramValues[10] = candidatePriorityStr.strValue; paramValues[11] = quorum ? "true" : "false"; paramValues[12] = IS_EMPTY_STRING_BUFFER(citusClusterName) @@ -945,13 +957,16 @@ monitor_node_active(Monitor *monitor, MonitorAssignedStateParseContext parseContext = { { 0 }, assignedState, false }; const char *nodeStateString = NodeStateToString(currentState); + IntString nodeIdString = intToString(nodeId); + IntString groupIdString = intToString(groupId); + IntString currentTLIString = intToString(currentTLI); paramValues[0] = formation; - paramValues[1] = intToString(nodeId).strValue; - paramValues[2] = intToString(groupId).strValue; + paramValues[1] = nodeIdString.strValue; + paramValues[2] = groupIdString.strValue; paramValues[3] = nodeStateString; paramValues[4] = pgIsRunning ? "true" : "false"; - paramValues[5] = intToString(currentTLI).strValue; + paramValues[5] = currentTLIString.strValue; paramValues[6] = currentLSN; paramValues[7] = pgsrSyncState; @@ -1001,7 +1016,8 @@ monitor_set_node_candidate_priority(Monitor *monitor, int paramCount = 3; Oid paramTypes[3] = { TEXTOID, TEXTOID, INT4OID }; const char *paramValues[3]; - char *candidatePriorityText = intToString(candidate_priority).strValue; + IntString candidatePriorityString = intToString(candidate_priority); + char *candidatePriorityText = candidatePriorityString.strValue; bool success = true; paramValues[0] = formation; @@ -1218,8 +1234,9 @@ monitor_set_formation_number_sync_standbys(Monitor *monitor, char *formation, Oid paramTypes[2] = { TEXTOID, INT4OID }; const char *paramValues[2]; SingleValueResultContext parseContext = { { 0 }, PGSQL_RESULT_BOOL, false }; + IntString numberSyncStandbysString = intToString(numberSyncStandbys); paramValues[0] = formation; - paramValues[1] = intToString(numberSyncStandbys).strValue; + paramValues[1] = numberSyncStandbysString.strValue; if (!pgsql_execute_with_params(pgsql, sql, paramCount, paramTypes, paramValues, @@ -1256,9 +1273,10 @@ monitor_remove_by_hostname(Monitor *monitor, char *host, int port, bool force, int paramCount = 3; Oid paramTypes[3] = { TEXTOID, INT4OID, BOOLOID }; const char *paramValues[3]; + IntString portString = intToString(port); paramValues[0] = host; - paramValues[1] = intToString(port).strValue; + paramValues[1] = portString.strValue; paramValues[2] = force ? "true" : "false"; if (!pgsql_execute_with_params(pgsql, sql, @@ -1495,9 +1513,10 @@ monitor_perform_failover(Monitor *monitor, char *formation, int group) int paramCount = 2; Oid paramTypes[2] = { TEXTOID, INT4OID }; const char *paramValues[2]; + IntString groupString = intToString(group); paramValues[0] = formation; - paramValues[1] = intToString(group).strValue; + paramValues[1] = groupString.strValue; /* * pgautofailover.perform_failover() returns VOID. @@ -2738,12 +2757,13 @@ monitor_create_formation(Monitor *monitor, int paramCount = 5; Oid paramTypes[5] = { TEXTOID, TEXTOID, TEXTOID, BOOLOID, INT4OID }; const char *paramValues[5]; + IntString numberSyncStandbysString = intToString(numberSyncStandbys); paramValues[0] = formation; paramValues[1] = kind; paramValues[2] = dbname; paramValues[3] = hasSecondary ? "true" : "false"; - paramValues[4] = intToString(numberSyncStandbys).strValue; + paramValues[4] = numberSyncStandbysString.strValue; if (!pgsql_execute_with_params(pgsql, sql, paramCount, paramTypes, paramValues, @@ -3429,11 +3449,13 @@ monitor_update_node_metadata(Monitor *monitor, const char *paramValues[4]; SingleValueResultContext context = { { 0 }, PGSQL_RESULT_BOOL, false }; + IntString nodeIdString = intToString(nodeId); + IntString portString = intToString(port); - paramValues[0] = intToString(nodeId).strValue; + paramValues[0] = nodeIdString.strValue; paramValues[1] = name; paramValues[2] = hostname; - paramValues[3] = intToString(port).strValue; + paramValues[3] = portString.strValue; if (!pgsql_execute_with_params(pgsql, sql, paramCount, paramTypes, paramValues, @@ -3477,9 +3499,11 @@ monitor_set_node_system_identifier(Monitor *monitor, NodeAddress node = { 0 }; NodeAddressParseContext parseContext = { { 0 }, &node, false }; + IntString nodeIdString = intToString(nodeId); + IntString systemIdentifierString = intToString(system_identifier); - paramValues[0] = intToString(nodeId).strValue; - paramValues[1] = intToString(system_identifier).strValue; + paramValues[0] = nodeIdString.strValue; + paramValues[1] = systemIdentifierString.strValue; if (!pgsql_execute_with_params(pgsql, sql, paramCount, paramTypes, paramValues, @@ -3525,9 +3549,11 @@ monitor_set_group_system_identifier(Monitor *monitor, const char *paramValues[2]; SingleValueResultContext context = { 0 }; + IntString groupIdString = intToString(groupId); + IntString systemIdentifierString = intToString(system_identifier); - paramValues[0] = intToString(groupId).strValue; - paramValues[1] = intToString(system_identifier).strValue; + paramValues[0] = groupIdString.strValue; + paramValues[1] = systemIdentifierString.strValue; if (!pgsql_execute_with_params(pgsql, sql, paramCount, paramTypes, paramValues, @@ -3640,8 +3666,9 @@ monitor_start_maintenance(Monitor *monitor, int64_t nodeId, bool *mayRetry) int paramCount = 1; Oid paramTypes[1] = { INT8OID }; const char *paramValues[1]; + IntString nodeIdString = intToString(nodeId); - paramValues[0] = intToString(nodeId).strValue; + paramValues[0] = nodeIdString.strValue; if (!pgsql_execute_with_params(pgsql, sql, paramCount, paramTypes, paramValues, @@ -3688,8 +3715,9 @@ monitor_stop_maintenance(Monitor *monitor, int64_t nodeId, bool *mayRetry) int paramCount = 1; Oid paramTypes[1] = { INT8OID }; const char *paramValues[1]; + IntString nodeIdString = intToString(nodeId); - paramValues[0] = intToString(nodeId).strValue; + paramValues[0] = nodeIdString.strValue; if (!pgsql_execute_with_params(pgsql, sql, paramCount, paramTypes, paramValues, @@ -4854,10 +4882,12 @@ monitor_find_node_by_nodeid(Monitor *monitor, const char *paramValues[3]; NodeAddressArrayParseContext parseContext = { { 0 }, nodesArray, false }; + IntString groupIdString = intToString(groupId); + IntString nodeIdString = intToString(nodeId); paramValues[0] = formation; - paramValues[1] = intToString(groupId).strValue; - paramValues[2] = intToString(nodeId).strValue; + paramValues[1] = groupIdString.strValue; + paramValues[2] = nodeIdString.strValue; if (!pgsql_execute_with_params(pgsql, sql, paramCount, paramTypes, paramValues, diff --git a/tests/network.py b/tests/network.py index 338d7297b..02e2d91f8 100644 --- a/tests/network.py +++ b/tests/network.py @@ -162,7 +162,8 @@ def run(self, command, user=os.getenv("USER")): stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, - universal_newlines=True, + encoding="utf-8", + errors="replace", start_new_session=True, ) @@ -190,7 +191,8 @@ def run_unmanaged(self, command, user=os.getenv("USER")): stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, - universal_newlines=True, + encoding="utf-8", + errors="replace", start_new_session=True, ) From a913eab1b69cd1cc81e7e49fb84b5754dae11777 Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Thu, 9 Jul 2026 02:21:39 +0200 Subject: [PATCH 66/68] ci: rebalance pgaftest schedules, gate all jobs on style check Split multi-1/multi-2 into three focused schedules: - multi-alternate: multi_alternate alone (~6m) - multi-async: multi_async alone (~6m) - multi-misc: multi_standbys + multi_maintenance + ensure + multi_ifdown (~8m) This brings the slow-schedule ceiling from 10.6m down to ~8m, with quick and node (~7m each) now the practical floor. Rebalance citus schedules: - citus-1: citus_cluster_name + citus_force_failover + citus_multi_standbys (~4m) - citus-2: basic_citus_operation + nonha_citus_operation + citus_skip_pg_hba (~6m) Add style_checker job to run-pgaftest.yml (was only in run-tests.yml), and gate both build-images and build-pgaftest on it passing. Also gate build_images in run-tests.yml on style_checker. --- .github/workflows/run-pgaftest.yml | 30 ++++++++++++++++++++----- .github/workflows/run-tests.yml | 1 + tests/tap/schedules/citus-1.sch | 4 ++-- tests/tap/schedules/citus-2.sch | 4 ++-- tests/tap/schedules/multi-1.sch | 4 ---- tests/tap/schedules/multi-2.sch | 4 ---- tests/tap/schedules/multi-alternate.sch | 2 ++ tests/tap/schedules/multi-async.sch | 2 ++ tests/tap/schedules/multi-misc.sch | 5 +++++ 9 files changed, 39 insertions(+), 17 deletions(-) delete mode 100644 tests/tap/schedules/multi-1.sch delete mode 100644 tests/tap/schedules/multi-2.sch create mode 100644 tests/tap/schedules/multi-alternate.sch create mode 100644 tests/tap/schedules/multi-async.sch create mode 100644 tests/tap/schedules/multi-misc.sch diff --git a/.github/workflows/run-pgaftest.yml b/.github/workflows/run-pgaftest.yml index 28fd1079a..a3b128d05 100644 --- a/.github/workflows/run-pgaftest.yml +++ b/.github/workflows/run-pgaftest.yml @@ -15,12 +15,30 @@ concurrency: cancel-in-progress: true jobs: + style_checker: + name: Style check + runs-on: ubuntu-latest + container: citus/stylechecker:no-py + steps: + - name: Checkout repository + uses: actions/checkout@v7.0.0 + + - name: Set safe directory for git + run: git config --global --add safe.directory ${GITHUB_WORKSPACE} + + - name: Check C formatting + run: citus_indent --check + + - name: Check banned functions + run: ci/banned.h.sh + # --------------------------------------------------------------------------- # Build one pgaf:run image per Postgres version and upload each as an # artifact. All downstream test jobs download only the version they need. # --------------------------------------------------------------------------- build-images: name: Build image (PG${{ matrix.PGVERSION }}) + needs: style_checker runs-on: ubuntu-latest strategy: fail-fast: false @@ -96,6 +114,7 @@ jobs: # --------------------------------------------------------------------------- build-pgaftest: name: Build pgaftest image + needs: style_checker runs-on: ubuntu-latest env: PGVERSION: 17 @@ -141,7 +160,7 @@ jobs: # Fast/medium schedules run against all four Postgres versions. # Slow schedules (multi-*, citus-*) run PG17 only: they exercise # pg_auto_failover FSM logic, not Postgres version-specific code paths, - # and restricting them reduces total job count from 36 to 16 — avoiding + # and restricting them reduces total job count from 36 to 17 — avoiding # GitHub Actions shared-runner queue saturation that was adding 10-15 min # of queue wait to every run. include: @@ -163,10 +182,11 @@ jobs: - { PGVERSION: 16, schedule: ssl } - { PGVERSION: 17, schedule: ssl } # slow schedules: PG17 only - - { PGVERSION: 17, schedule: multi-1 } # multi_standbys, multi_maintenance, multi_async - - { PGVERSION: 17, schedule: multi-2 } # ensure, multi_alternate, multi_ifdown - - { PGVERSION: 17, schedule: citus-1 } # citus_cluster_name, citus_force_failover, citus_skip_pg_hba - - { PGVERSION: 17, schedule: citus-2 } # basic_citus_operation, nonha_citus_operation, citus_multi_standbys + - { PGVERSION: 17, schedule: multi-alternate } # multi_alternate + - { PGVERSION: 17, schedule: multi-misc } # multi_standbys, multi_maintenance, ensure, multi_ifdown + - { PGVERSION: 17, schedule: multi-async } # multi_async + - { PGVERSION: 17, schedule: citus-1 } # citus_cluster_name, citus_force_failover, citus_multi_standbys + - { PGVERSION: 17, schedule: citus-2 } # basic_citus_operation, nonha_citus_operation, citus_skip_pg_hba steps: - uses: actions/checkout@v7.0.0 diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 6ca92f83f..322561aa4 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -30,6 +30,7 @@ jobs: build_images: name: Build test image (PG${{ matrix.PGVERSION }}) + needs: style_checker runs-on: ubuntu-latest strategy: fail-fast: false diff --git a/tests/tap/schedules/citus-1.sch b/tests/tap/schedules/citus-1.sch index b25f99030..f2f780e8e 100644 --- a/tests/tap/schedules/citus-1.sch +++ b/tests/tap/schedules/citus-1.sch @@ -1,4 +1,4 @@ -# Citus basic tests: cluster name, forced failover, skip-pg-hba (~15 min) +# Citus basic tests: cluster name, forced failover, multi-standby (~4 min) citus_cluster_name citus_force_failover -citus_skip_pg_hba +citus_multi_standbys diff --git a/tests/tap/schedules/citus-2.sch b/tests/tap/schedules/citus-2.sch index 58436119d..cf792a98c 100644 --- a/tests/tap/schedules/citus-2.sch +++ b/tests/tap/schedules/citus-2.sch @@ -1,4 +1,4 @@ -# Citus advanced tests: multi-standby and full HA operation (~20 min) +# Citus advanced tests: full HA, non-HA, skip-pg-hba (~6 min) basic_citus_operation nonha_citus_operation -citus_multi_standbys +citus_skip_pg_hba diff --git a/tests/tap/schedules/multi-1.sch b/tests/tap/schedules/multi-1.sch deleted file mode 100644 index 6529cbd4d..000000000 --- a/tests/tap/schedules/multi-1.sch +++ /dev/null @@ -1,4 +0,0 @@ -# Multi-node HA: standbys, maintenance, async replication (~18 min) -multi_standbys -multi_maintenance -multi_async diff --git a/tests/tap/schedules/multi-2.sch b/tests/tap/schedules/multi-2.sch deleted file mode 100644 index 685a7cf8b..000000000 --- a/tests/tap/schedules/multi-2.sch +++ /dev/null @@ -1,4 +0,0 @@ -# Multi-node failover: ensure, alternate failover, network partition (~20 min) -ensure -multi_alternate -multi_ifdown diff --git a/tests/tap/schedules/multi-alternate.sch b/tests/tap/schedules/multi-alternate.sch new file mode 100644 index 000000000..123f0cd95 --- /dev/null +++ b/tests/tap/schedules/multi-alternate.sch @@ -0,0 +1,2 @@ +# Multi-node alternate failover scenarios (~6 min) +multi_alternate diff --git a/tests/tap/schedules/multi-async.sch b/tests/tap/schedules/multi-async.sch new file mode 100644 index 000000000..eb8bb56d8 --- /dev/null +++ b/tests/tap/schedules/multi-async.sch @@ -0,0 +1,2 @@ +# Multi-node async replication scenarios (~6 min) +multi_async diff --git a/tests/tap/schedules/multi-misc.sch b/tests/tap/schedules/multi-misc.sch new file mode 100644 index 000000000..6f4e12177 --- /dev/null +++ b/tests/tap/schedules/multi-misc.sch @@ -0,0 +1,5 @@ +# Multi-node misc: standbys, maintenance, ensure, network partition (~8 min) +multi_standbys +multi_maintenance +ensure +multi_ifdown From 0d16d2e3fb28ef1fc523087a5119068bfd8122f5 Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Thu, 9 Jul 2026 02:52:27 +0200 Subject: [PATCH 67/68] src: delete dead azure #if 0 block from cli_do_root.c MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The azure command stubs were already guarded by #if 0 after the initial removal. Delete them outright — dead code hidden behind preprocessor guards is worse than no code at all. --- src/bin/pg_autoctl/cli_do_root.c | 192 ------------------------------- src/bin/pg_autoctl/cli_do_root.h | 2 - 2 files changed, 194 deletions(-) diff --git a/src/bin/pg_autoctl/cli_do_root.c b/src/bin/pg_autoctl/cli_do_root.c index 665d69521..0b473d808 100644 --- a/src/bin/pg_autoctl/cli_do_root.c +++ b/src/bin/pg_autoctl/cli_do_root.c @@ -385,198 +385,6 @@ CommandLine do_tmux_commands = "Set of facilities to handle tmux interactive sessions", NULL, NULL, NULL, do_tmux); -/* - * Azure integration has been removed. The commands that were here are no - * longer maintained and have been deleted from this file. See pgaftest for - * the replacement QA tooling. - */ - -#if 0 /* REMOVED: azure commands — see pgaftest instead */ -CommandLine do_azure_provision_region = - make_command("region", - "Provision an azure region: resource group, network, VMs", - "[option ...]", - " --prefix azure group name prefix (ha-demo)\n" - " --region name to use for referencing the region\n" - " --location azure location where to create a resource group\n" - " --monitor should we create a monitor in the region (false)\n" - " --nodes number of Postgres nodes to create (2)\n" - " --script output a shell script instead of creating resources\n", - cli_do_azure_getopts, - cli_do_azure_create_region); - -CommandLine do_azure_provision_nodes = - make_command("nodes", - "Provision our pre-created VM with pg_autoctl Postgres nodes", - "[option ...]", - " --prefix azure group name prefix (ha-demo)\n" - " --region name to use for referencing the region\n" - " --monitor should we create a monitor in the region (false)\n" - " --nodes number of Postgres nodes to create (2)\n" - " --script output a shell script instead of creating resources\n", - cli_do_azure_getopts, - cli_do_azure_create_nodes); - - -CommandLine *do_azure_provision[] = { - &do_azure_provision_region, - &do_azure_provision_nodes, - NULL -}; - -CommandLine do_azure_provision_commands = - make_command_set("provision", - "provision azure resources for a pg_auto_failover demo", - NULL, NULL, NULL, do_azure_provision); - -CommandLine do_azure_create = - make_command("create", - "Create an azure QA environment", - "[option ...]", - " --prefix azure group name prefix (ha-demo)\n" - " --region name to use for referencing the region\n" - " --location azure location to use for the resources\n" - " --nodes number of Postgres nodes to create (2)\n" - " --script output a script instead of creating resources\n" - " --no-monitor do not create the pg_autoctl monitor node\n" - " --no-app do not create the application node\n" - " --cidr use the 10.CIDR.CIDR.0/24 subnet (11)\n" - " --from-source provision pg_auto_failover from sources\n", - cli_do_azure_getopts, - cli_do_azure_create_environment); - -CommandLine do_azure_drop = - make_command("drop", - "Drop an azure QA environment: resource group, network, VMs", - "[option ...]", - " --prefix azure group name prefix (ha-demo)\n" - " --region name to use for referencing the region\n" - " --location azure location where to create a resource group\n" - " --monitor should we create a monitor in the region (false)\n" - " --nodes number of Postgres nodes to create (2)\n" - " --script output a shell script instead of creating resources\n", - cli_do_azure_getopts, - cli_do_azure_drop_region); - -CommandLine do_azure_deploy = - make_command("deploy", - "Deploy a pg_autoctl VMs, given by name", - "[option ...] vmName", - "", - cli_do_azure_getopts, - cli_do_azure_deploy); - -CommandLine do_azure_show_ips = - make_command("ips", - "Show public and private IP addresses for selected VMs", - "[option ...]", - " --prefix azure group name prefix (ha-demo)\n" - " --region name to use for referencing the region\n", - cli_do_azure_getopts, - cli_do_azure_show_ips); - -CommandLine do_azure_show_state = - make_command("state", - "Connect to the monitor node to show the current state", - "[option ...]", - " --prefix azure group name prefix (ha-demo)\n" - " --region name to use for referencing the region\n" - " --watch run the command again every 0.2s\n", - cli_do_azure_getopts, - cli_do_azure_show_state); - -CommandLine *do_azure_show[] = { - &do_azure_show_ips, - &do_azure_show_state, - NULL -}; - -CommandLine do_azure_show_commands = - make_command_set("show", - "show azure resources for a pg_auto_failover demo", - NULL, NULL, NULL, do_azure_show); - -CommandLine do_azure_ls = - make_command("ls", - "List resources in a given azure region", - "[option ...]", - " --prefix azure group name prefix (ha-demo)\n" - " --region name to use for referencing the region\n", - cli_do_azure_getopts, - cli_do_azure_ls); - -CommandLine do_azure_ssh = - make_command("ssh", - "Runs ssh -l ha-admin for a given VM name", - "[option ...]", - " --prefix azure group name prefix (ha-demo)\n" - " --region name to use for referencing the region\n", - cli_do_azure_getopts, - cli_do_azure_ssh); - -CommandLine do_azure_sync = - make_command("sync", - "Rsync pg_auto_failover sources on all the target region VMs", - "[option ...]", - " --prefix azure group name prefix (ha-demo)\n" - " --region region to use for referencing the region\n" - " --monitor should we create a monitor in the region (false)\n" - " --nodes number of Postgres nodes to create (2)\n", - cli_do_azure_getopts, - cli_do_azure_rsync); - -CommandLine do_azure_tmux_session = - make_command("session", - "Create or attach a tmux session for the created Azure VMs", - "[option ...]", - " --prefix azure group name prefix (ha-demo)\n" - " --region region to use for referencing the region\n" - " --monitor should we create a monitor in the region (false)\n" - " --nodes number of Postgres nodes to create (2)\n", - cli_do_azure_getopts, - cli_do_azure_tmux_session); - -CommandLine do_azure_tmux_kill = - make_command("kill", - "Kill an existing tmux session for Azure VMs", - "[option ...]", - " --prefix azure group name prefix (ha-demo)\n" - " --region region to use for referencing the region\n" - " --monitor should we create a monitor in the region (false)\n" - " --nodes number of Postgres nodes to create (2)\n", - cli_do_azure_getopts, - cli_do_azure_tmux_kill); - -CommandLine *do_azure_tmux[] = { - &do_azure_tmux_session, - &do_azure_tmux_kill, - NULL -}; - -CommandLine do_azure_tmux_commands = - make_command_set("tmux", - "Run a tmux session with an Azure setup for QA/testing", - NULL, NULL, NULL, do_azure_tmux); - -CommandLine *do_azure[] = { - &do_azure_provision_commands, - &do_azure_tmux_commands, - &do_azure_show_commands, - &do_azure_deploy, - &do_azure_create, - &do_azure_drop, - &do_azure_ls, - &do_azure_ssh, - &do_azure_sync, - NULL -}; - -CommandLine do_azure_commands = - make_command_set("azure", - "Manage a set of Azure resources for a pg_auto_failover demo", - NULL, NULL, NULL, do_azure); - -#endif /* REMOVED: azure commands */ /* * pg_autoctl internal service postgres|listener|node-active diff --git a/src/bin/pg_autoctl/cli_do_root.h b/src/bin/pg_autoctl/cli_do_root.h index ab6f5aa22..3cfe3af30 100644 --- a/src/bin/pg_autoctl/cli_do_root.h +++ b/src/bin/pg_autoctl/cli_do_root.h @@ -147,6 +147,4 @@ void cli_do_tmux_compose_config(int argc, char **argv); void cli_do_tmux_compose_script(int argc, char **argv); void cli_do_tmux_compose_session(int argc, char **argv); -/* Azure integration removed — use pgaftest instead */ - #endif /* CLI_DO_ROOT_H */ From 72b653d71ee34d30fa43b18306f89d4ba2d0ca4f Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Thu, 9 Jul 2026 03:03:32 +0200 Subject: [PATCH 68/68] cleanup: remove azure Makefile, pipenv, stale comments and wrong job count MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Delete Makefile.azure and its include/COPY references — the three make targets (azcluster, az, azdrop) called pg_autoctl do azure which was deleted; the file was pure dead weight - Drop pipenv from Dockerfile base apt-get block; nothing uses it - Fix job-count formula comment: 3×4 + 5×1 = 17 (not 16) - Remove tombstone comment from cli_demo.c file header - Drop stale log_info pointing users to deleted pg_autoctl do demo run --- .github/workflows/run-pgaftest.yml | 2 +- Dockerfile | 2 -- Makefile | 3 --- Makefile.azure | 38 ------------------------------ src/bin/pgaftest/cli_demo.c | 3 +-- 5 files changed, 2 insertions(+), 46 deletions(-) delete mode 100644 Makefile.azure diff --git a/.github/workflows/run-pgaftest.yml b/.github/workflows/run-pgaftest.yml index a3b128d05..b6a9085c8 100644 --- a/.github/workflows/run-pgaftest.yml +++ b/.github/workflows/run-pgaftest.yml @@ -147,7 +147,7 @@ jobs: # --------------------------------------------------------------------------- # Run test schedules. Fast schedules (quick, node, ssl) run against all four # Postgres versions. Slow schedules (multi-*, citus-*) run PG17 only. - # Total: 3×4 + 4×1 = 16 test jobs — keeps GitHub shared-runner queue short. + # Total: 3×4 + 5×1 = 17 test jobs — keeps GitHub shared-runner queue short. # --------------------------------------------------------------------------- test: name: pgaftest / ${{ matrix.schedule }} (PG${{ matrix.PGVERSION }}) diff --git a/Dockerfile b/Dockerfile index f999ef0c8..72fad8a7e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -44,7 +44,6 @@ RUN apt-get update \ make \ autoconf \ openssl \ - pipenv \ python3 \ python3-setuptools \ python3-psycopg2 \ @@ -107,7 +106,6 @@ ENV PG_CONFIG=/usr/lib/postgresql/${PGVERSION}/bin/pg_config WORKDIR /usr/src/pg_auto_failover COPY Makefile ./ -COPY Makefile.azure ./ COPY Makefile.citus ./ COPY ./src/ ./src COPY ./src/bin/pg_autoctl/git-version.h ./src/bin/pg_autoctl/git-version.h diff --git a/Makefile b/Makefile index 743f3b75d..47f05753e 100644 --- a/Makefile +++ b/Makefile @@ -33,9 +33,6 @@ else GIT_VERSION := $(shell awk -F '[ "]' '{print $$4}' $(VERSION_FILE)) endif -# Azure only targets and variables are in a separate Makefile -include Makefile.azure - # # LIST TESTS # diff --git a/Makefile.azure b/Makefile.azure deleted file mode 100644 index 1b29839b8..000000000 --- a/Makefile.azure +++ /dev/null @@ -1,38 +0,0 @@ -# -# AZURE related -# - -# make azcluster arguments -AZURE_PREFIX ?= ha-demo-$(shell whoami) -AZURE_REGION ?= paris -AZURE_LOCATION ?= francecentral - -# Pick a version of Postgres and pg_auto_failover packages to install -# in our target Azure VMs when provisionning -# -# sudo apt-get install -q -y postgresql-13-auto-failover-1.5=1.5.2 -# postgresql-${AZ_PG_VERSION}-auto-failover-${AZ_PGAF_DEB_VERSION}=${AZ_PGAF_VERSION} -AZ_PG_VERSION ?= 13 -AZ_PGAF_DEB_VERSION ?= 1.6 -AZ_PGAF_DEB_REVISION ?= 1.6.4-1 - -export AZ_PG_VERSION -export AZ_PGAF_DEB_VERSION -export AZ_PGAF_DEB_REVISION - -.PHONY: azcluster -azcluster: all - $(PG_AUTOCTL) do azure create \ - --prefix $(AZURE_PREFIX) \ - --region $(AZURE_REGION) \ - --location $(AZURE_LOCATION) \ - --nodes $(NODES) - -# make azcluster has been done before, just re-attach -.PHONY: az -az: all - $(PG_AUTOCTL) do azure tmux session - -.PHONY: azdrop -azdrop: all - $(PG_AUTOCTL) do azure drop diff --git a/src/bin/pgaftest/cli_demo.c b/src/bin/pgaftest/cli_demo.c index 8193b8b71..8a9a816a2 100644 --- a/src/bin/pgaftest/cli_demo.c +++ b/src/bin/pgaftest/cli_demo.c @@ -1,6 +1,6 @@ /* * src/bin/pgaftest/cli_demo.c - * Demo application sub-command for pgaftest (moved from pg_autoctl do demo). + * Demo application sub-command for pgaftest. * * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the PostgreSQL License. @@ -20,7 +20,6 @@ static void cli_demo_run(int argc, char **argv) { log_error("pgaftest demo run: not yet implemented in this build."); - log_info("Use: docker compose exec pg_autoctl do demo run ..."); }