Package:postgresql-common Verison:273.pgdg24.04+1 Dear Community,
Experimented with a script, pg_findallclusters referenced based on pg_lsclusters, to detect both system-managed (pg_createcluster) and manually created (initdb) PostgreSQL clusters. The goal is to improve visibility in environments where not all clusters are managed by Debian tools. Key Features & Use Case: - Detects All Running Clusters: Finds both system-managed and manually created clusters. - Assigns Names to Manual Clusters: Uses the data directory name as the cluster name. - Security & Efficiency: Runs only with sudo, minimizing privilege escalations. - Enhanced Output: Displays cluster port, owner, and size, with structured formatting. Since manually created clusters lack predefined names, using the data directory name provides a consistent and meaningful way to reference them without requiring modifications to PostgreSQL itself. This is just an experimental approach to explore feasibility. Below is the patch and an example output. Patch: #!/usr/bin/perl -w # Show both system-managed and manually created PostgreSQL clusters use strict; use Getopt::Long; use PgCommon; use File::Basename; sub help { print "Usage: $0 [-hjs] Options: -h --no-header Omit column headers in output -j --json JSON output -s --start-conf Include start.conf information in status column --help Print help\n"; exit 0; } # Command-line options my ($no_header, $json, $start_conf); help(1) unless GetOptions( 'help' => sub { help(0); }, 'h|no-header' => \$no_header, 'j|json' => \$json, 's|start-conf' => \$start_conf, ); # Detect if user has sudo privileges my $use_sudo = system("command -v sudo >/dev/null 2>&1 && sudo -n true >/dev/null 2>&1") == 0 ? 1 : 0; my $sudo_prefix = $use_sudo ? "sudo " : ""; # Safer command execution with reduced password prompts sub safe_exec { my ($cmd, $as_user, $ignore_errors) = @_; $ignore_errors //= 0; # Redirect stderr to /dev/null to suppress error messages $cmd .= " 2>/dev/null" unless $ignore_errors; if ($as_user) { # Use runuser instead of sudo to reduce password prompts if ($use_sudo) { return qx($sudo_prefix runuser -u $as_user -- sh -c '$cmd' 2>/dev/null); } else { return qx(su - $as_user -c '$cmd' 2>/dev/null); } } else { return qx($cmd); } } # Function to get port from config file when cluster is down sub get_port_from_config { my ($conf_file) = @_; return '' unless -f $conf_file; my $port = ''; if (open(my $fh, '<', $conf_file)) { while (<$fh>) { if (/^\s*port\s*=\s*(\d+)/) { $port = $1; last; } } close $fh; } return $port; } # Collect system-managed clusters my @sys_clusters; my $jsoninfo = []; foreach my $v (get_versions()) { my @clusters = get_version_clusters($v); foreach my $c (@clusters) { my %info = cluster_info($v, $c); my $status = $info{'running'} ? "online" : "down"; unless (-e "${PgCommon::binroot}$v/bin/postgres") { $status .= ",binaries_missing"; } my $conf_file = "/etc/postgresql/$v/$c/postgresql.conf"; # Get port - use configuration if not running my $port; if ($info{'running'}) { $port = $info{'port'} // '<unknown>'; } else { $port = get_port_from_config($conf_file); } # Determine Role (Primary/Replica) my $role = "Unknown"; if ($info{'running'}) { my $recovery_mode = safe_exec("psql -p $port -tAc \"SELECT pg_is_in_recovery()\"", "postgres", 1); chomp($recovery_mode); $role = ($recovery_mode eq 't') ? "Replica" : "Primary"; } else { my $standby_file = "$info{'pgdata'}/standby.signal"; $role = (-e $standby_file) ? "Replica" : "Primary"; } # Get Data Directory Size - Modified to work for both sudo and non-sudo users my $size; if ($info{'owneruid'} == $< || $use_sudo) { # If current user is the owner or has sudo privileges $size = safe_exec("$sudo_prefix du -sh $info{'pgdata'} | awk '{print \$1}'"); chomp($size); } else { # Try to get size from psql if the cluster is running if ($info{'running'} && $port) { my $db_size = safe_exec("psql -p $port -tAc \"SELECT pg_size_pretty(pg_database_size('postgres'))\"", "postgres", 1); chomp($db_size); if ($db_size) { $size = "$db_size+"; # Add '+' to indicate this is just the postgres DB size } } } $size ||= "Unknown"; push @sys_clusters, [$v, $c, $port, $status, (getpwuid $info{'owneruid'})[0] // 'postgres', $info{'pgdata'} || '<unknown>', $conf_file, $role, $size]; push @$jsoninfo, \%info; } } # Find PostgreSQL binaries sub find_pg_binaries { my @pg_binaries; # Check standard locations my @binary_locations = ( glob("/usr/lib/postgresql/*/bin"), glob("/usr/pgsql-*/bin"), glob("/usr/local/pgsql/bin"), glob("/opt/postgresql/*/bin"), "/usr/bin", "/usr/local/bin" ); foreach my $bin_path (@binary_locations) { if (-e "$bin_path/pg_ctl" && -x "$bin_path/pg_ctl") { push @pg_binaries, "$bin_path/pg_ctl"; } } # Also check in PATH my $path_pg_ctl = qx(which pg_ctl 2>/dev/null); chomp($path_pg_ctl); if ($path_pg_ctl && -x $path_pg_ctl) { push @pg_binaries, $path_pg_ctl; } return @pg_binaries; } # Improved function to find only valid PostgreSQL data directories sub find_postgres_data_dirs { my @data_dirs; my %seen; # Get list of PostgreSQL processes my $ps_cmd = "ps aux | grep -E 'postgres.*-D' | grep -v grep"; my @running_pg = qx($ps_cmd); chomp @running_pg; # Extract data directories from running instances foreach my $process (@running_pg) { if ($process =~ /-D\s+(\S+)/) { my $data_dir = $1; if (-d $data_dir && -f "$data_dir/PG_VERSION" && !$seen{$data_dir}++) { push @data_dirs, $data_dir; } } } # Check network connections more safely my $netstat_cmd = "ss -tulnp 2>/dev/null | grep postgres"; my @netstat_out = qx($netstat_cmd); my %seen_ports; foreach my $line (@netstat_out) { if ($line =~ /:(\d+)\s/) { my $port = $1; next if $seen_ports{$port}++; # Use runuser to reduce password prompts my $data_dir = safe_exec("psql -p $port -tAc \"SHOW data_directory\"", "postgres", 1); chomp($data_dir); if ($data_dir && -d $data_dir && -f "$data_dir/PG_VERSION" && !$seen{$data_dir}++) { push @data_dirs, $data_dir; } } } # Check common paths safely my @common_paths = ( glob("/var/lib/postgresql/*/main"), glob("/var/lib/pgsql/*/data"), glob("/opt/postgres*"), glob("/opt/pgsql*/data"), glob("/usr/local/pgsql/data"), glob("/home/*/postgres*") ); foreach my $path (@common_paths) { if (-d $path && -f "$path/PG_VERSION" && !$seen{$path}++) { push @data_dirs, $path; } } return keys %seen; # Return unique paths } # More reliable manual cluster detection sub get_manual_clusters { my %ports; my $output = qx(ss -tulnp | grep postgres 2>/dev/null); foreach my $line (split /\n/, $output) { if ($line =~ /:(\d+)\s/) { my $port = $1; my $data_dir = safe_exec("psql -p $port -tAc \"SHOW data_directory\"", "postgres", 1); chomp($data_dir); $ports{$data_dir} = $port if $data_dir; } } my @manual_clusters; my %seen; # Filter out system-managed clusters foreach my $sys_cluster (@sys_clusters) { $seen{$sys_cluster->[5]} = 1; } # Auto-detect PostgreSQL data directories my @cluster_dirs = find_postgres_data_dirs(); foreach my $data_dir (@cluster_dirs) { next if $seen{$data_dir}; # Skip if already processed next unless -d $data_dir; next unless -f "$data_dir/PG_VERSION"; # Must be a PostgreSQL data directory $seen{$data_dir} = 1; my $status = exists $ports{$data_dir} ? "online" : "down"; my $conf_file = "$data_dir/postgresql.conf"; # Get port - if down, try to read from config my $port; if ($status eq "online") { $port = $ports{$data_dir} // ''; } else { $port = get_port_from_config($conf_file); } # Get PostgreSQL version my $major_version = "unknown"; if ($status eq "online") { my $version_output = safe_exec("psql -p $port -tAc \"SHOW server_version\"", "postgres", 1); chomp($version_output); ($major_version) = $version_output =~ /^(\d+)/; } else { my $pg_version_file = "$data_dir/PG_VERSION"; if (-e $pg_version_file) { my $file_version = qx(cat $pg_version_file); chomp($file_version); ($major_version) = $file_version =~ /^(\d+)/; } } # Extract cluster name from data directory my $cluster_name = basename($data_dir); my $owner = qx(stat -c '%U' "$data_dir" 2>/dev/null); chomp($owner); $owner ||= 'postgres'; # Determine Role my $role = "Unknown"; if ($status eq "online") { my $recovery_mode = safe_exec("psql -p $port -tAc \"SELECT pg_is_in_recovery()\"", "postgres", 1); chomp($recovery_mode); $role = ($recovery_mode eq 't') ? "Replica" : "Primary"; } else { my $standby_file = "$data_dir/standby.signal"; $role = (-e $standby_file) ? "Replica" : "Primary"; } # Get Data Directory Size with reduced interaction my $size; my $uid = (stat($data_dir))[4]; if ($uid == $< || $use_sudo) { $size = safe_exec("$sudo_prefix du -sh $data_dir | awk '{print \$1}'"); chomp($size); } else { $size = "Unknown"; } push @manual_clusters, [$major_version, $cluster_name, $port, $status, $owner, $data_dir, $conf_file, $role, $size]; } return @manual_clusters; } # Prepare output using column command sub format_output { my ($title, $clusters) = @_; # Create temporary files my $header_file = "/tmp/pg_header_$$"; my $data_file = "/tmp/pg_data_$$"; # Define header open(my $header_fh, '>', $header_file) or die "Cannot create temp file: $!"; print $header_fh "Ver\tCluster\tPort\tStatus\tOwner\tData_Directory\tConfig_File\tRole\tSize\n"; close $header_fh; # Write data open(my $data_fh, '>', $data_file) or die "Cannot create temp file: $!"; foreach my $line (@$clusters) { print $data_fh join("\t", @$line) . "\n"; } close $data_fh; # Print title print "\n\033[1;34m$title\033[0m\n"; # Format using column my $column_cmd = "cat $header_file $data_file | column -t -s '\t'"; my @formatted = qx($column_cmd); # Print header print "\033[1m$formatted[0]\033[0m"; print "-" x length($formatted[0]) . "\n"; # Print data with colors for (my $i = 1; $i < scalar(@formatted); $i++) { my @fields = split(/\s{2,}/, $formatted[$i]); my $status = $fields[3]; my $color = ($status =~ /^online/) ? "\033[32m" : "\033[31m"; print "${color}$formatted[$i]\033[0m"; } # Clean up unlink $header_file; unlink $data_file; } # Collect manual clusters first my @manual_clusters = get_manual_clusters(); # Format and display clusters format_output("Debian PostgreSQL Clusters (pg_createcluster)", \@sys_clusters); format_output("Manually Created Clusters (initdb)", \@manual_clusters); Output: [image: image.png]