LCOV - code coverage report
Current view: top level - compute_tools/src - config.rs (source / functions) Coverage Total Hit
Test: 1d5975439f3c9882b18414799141ebf9a3922c58.info Lines: 7.6 % 237 18
Test Date: 2025-07-31 15:59:03 Functions: 16.7 % 6 1

            Line data    Source code
       1              : use anyhow::Result;
       2              : use std::fmt::Write as FmtWrite;
       3              : use std::fs::{File, OpenOptions};
       4              : use std::io;
       5              : use std::io::Write;
       6              : use std::io::prelude::*;
       7              : use std::path::Path;
       8              : 
       9              : use compute_api::responses::TlsConfig;
      10              : use compute_api::spec::{
      11              :     ComputeAudit, ComputeMode, ComputeSpec, DatabricksSettings, GenericOption,
      12              : };
      13              : 
      14              : use crate::compute::ComputeNodeParams;
      15              : use crate::pg_helpers::{
      16              :     DatabricksSettingsExt as _, GenericOptionExt, GenericOptionsSearch, PgOptionsSerialize,
      17              :     escape_conf_value,
      18              : };
      19              : use crate::tls::{self, SERVER_CRT, SERVER_KEY};
      20              : 
      21              : use utils::shard::{ShardIndex, ShardNumber};
      22              : 
      23              : /// Check that `line` is inside a text file and put it there if it is not.
      24              : /// Create file if it doesn't exist.
      25            3 : pub fn line_in_file(path: &Path, line: &str) -> Result<bool> {
      26            3 :     let mut file = OpenOptions::new()
      27            3 :         .read(true)
      28            3 :         .write(true)
      29            3 :         .create(true)
      30            3 :         .append(false)
      31            3 :         .truncate(false)
      32            3 :         .open(path)?;
      33            3 :     let buf = io::BufReader::new(&file);
      34            3 :     let mut count: usize = 0;
      35              : 
      36            5 :     for l in buf.lines() {
      37            5 :         if l? == line {
      38            1 :             return Ok(false);
      39            4 :         }
      40            4 :         count = 1;
      41              :     }
      42              : 
      43            2 :     write!(file, "{}{}", "\n".repeat(count), line)?;
      44            2 :     Ok(true)
      45            3 : }
      46              : 
      47              : /// Create or completely rewrite configuration file specified by `path`
      48              : #[allow(clippy::too_many_arguments)]
      49            0 : pub fn write_postgres_conf(
      50            0 :     pgdata_path: &Path,
      51            0 :     params: &ComputeNodeParams,
      52            0 :     spec: &ComputeSpec,
      53            0 :     postgres_port: Option<u16>,
      54            0 :     extension_server_port: u16,
      55            0 :     tls_config: &Option<TlsConfig>,
      56            0 :     databricks_settings: Option<&DatabricksSettings>,
      57            0 :     lakebase_mode: bool,
      58            0 : ) -> Result<()> {
      59            0 :     let path = pgdata_path.join("postgresql.conf");
      60              :     // File::create() destroys the file content if it exists.
      61            0 :     let mut file = File::create(path)?;
      62              : 
      63              :     // Write the postgresql.conf content from the spec file as is.
      64            0 :     if let Some(conf) = &spec.cluster.postgresql_conf {
      65            0 :         writeln!(file, "{conf}")?;
      66            0 :     }
      67              : 
      68              :     // Add options for connecting to storage
      69            0 :     writeln!(file, "# Neon storage settings")?;
      70            0 :     writeln!(file)?;
      71            0 :     if let Some(conninfo) = &spec.pageserver_connection_info {
      72              :         // Stripe size GUC should be defined prior to connection string
      73            0 :         if let Some(stripe_size) = conninfo.stripe_size {
      74            0 :             writeln!(
      75            0 :                 file,
      76            0 :                 "# from compute spec's pageserver_connection_info.stripe_size field"
      77            0 :             )?;
      78            0 :             writeln!(file, "neon.stripe_size={stripe_size}")?;
      79            0 :         }
      80              : 
      81            0 :         let mut libpq_urls: Option<Vec<String>> = Some(Vec::new());
      82            0 :         let num_shards = if conninfo.shard_count.0 == 0 {
      83            0 :             1 // unsharded, treat it as a single shard
      84              :         } else {
      85            0 :             conninfo.shard_count.0
      86              :         };
      87              : 
      88            0 :         for shard_number in 0..num_shards {
      89            0 :             let shard_index = ShardIndex {
      90            0 :                 shard_number: ShardNumber(shard_number),
      91            0 :                 shard_count: conninfo.shard_count,
      92            0 :             };
      93            0 :             let info = conninfo.shards.get(&shard_index).ok_or_else(|| {
      94            0 :                 anyhow::anyhow!(
      95            0 :                     "shard {shard_index} missing from pageserver_connection_info shard map"
      96              :                 )
      97            0 :             })?;
      98              : 
      99            0 :             let first_pageserver = info
     100            0 :                 .pageservers
     101            0 :                 .first()
     102            0 :                 .expect("must have at least one pageserver");
     103              : 
     104              :             // Add the libpq URL to the array, or if the URL is missing, reset the array
     105              :             // forgetting any previous entries. All servers must have a libpq URL, or none
     106              :             // at all.
     107            0 :             if let Some(url) = &first_pageserver.libpq_url {
     108            0 :                 if let Some(ref mut urls) = libpq_urls {
     109            0 :                     urls.push(url.clone());
     110            0 :                 }
     111              :             } else {
     112            0 :                 libpq_urls = None
     113              :             }
     114              :         }
     115            0 :         if let Some(libpq_urls) = libpq_urls {
     116            0 :             writeln!(
     117            0 :                 file,
     118            0 :                 "# derived from compute spec's pageserver_connection_info field"
     119            0 :             )?;
     120            0 :             writeln!(
     121            0 :                 file,
     122            0 :                 "neon.pageserver_connstring={}",
     123            0 :                 escape_conf_value(&libpq_urls.join(","))
     124            0 :             )?;
     125              :         } else {
     126            0 :             writeln!(file, "# no neon.pageserver_connstring")?;
     127              :         }
     128              :     } else {
     129              :         // Stripe size GUC should be defined prior to connection string
     130            0 :         if let Some(stripe_size) = spec.shard_stripe_size {
     131            0 :             writeln!(file, "# from compute spec's shard_stripe_size field")?;
     132            0 :             writeln!(file, "neon.stripe_size={stripe_size}")?;
     133            0 :         }
     134            0 :         if let Some(s) = &spec.pageserver_connstring {
     135            0 :             writeln!(file, "# from compute spec's pageserver_connstring field")?;
     136            0 :             writeln!(file, "neon.pageserver_connstring={}", escape_conf_value(s))?;
     137            0 :         }
     138              :     }
     139              : 
     140            0 :     if !spec.safekeeper_connstrings.is_empty() {
     141            0 :         let mut neon_safekeepers_value = String::new();
     142            0 :         tracing::info!(
     143            0 :             "safekeepers_connstrings is not zero, gen: {:?}",
     144              :             spec.safekeepers_generation
     145              :         );
     146              :         // If generation is given, prepend sk list with g#number:
     147            0 :         if let Some(generation) = spec.safekeepers_generation {
     148            0 :             write!(neon_safekeepers_value, "g#{generation}:")?;
     149            0 :         }
     150            0 :         neon_safekeepers_value.push_str(&spec.safekeeper_connstrings.join(","));
     151            0 :         writeln!(
     152            0 :             file,
     153            0 :             "neon.safekeepers={}",
     154            0 :             escape_conf_value(&neon_safekeepers_value)
     155            0 :         )?;
     156            0 :     }
     157            0 :     if let Some(s) = &spec.tenant_id {
     158            0 :         writeln!(file, "neon.tenant_id={}", escape_conf_value(&s.to_string()))?;
     159            0 :     }
     160            0 :     if let Some(s) = &spec.timeline_id {
     161            0 :         writeln!(
     162            0 :             file,
     163            0 :             "neon.timeline_id={}",
     164            0 :             escape_conf_value(&s.to_string())
     165            0 :         )?;
     166            0 :     }
     167            0 :     if let Some(s) = &spec.project_id {
     168            0 :         writeln!(file, "neon.project_id={}", escape_conf_value(s))?;
     169            0 :     }
     170            0 :     if let Some(s) = &spec.branch_id {
     171            0 :         writeln!(file, "neon.branch_id={}", escape_conf_value(s))?;
     172            0 :     }
     173            0 :     if let Some(s) = &spec.endpoint_id {
     174            0 :         writeln!(file, "neon.endpoint_id={}", escape_conf_value(s))?;
     175            0 :     }
     176              : 
     177              :     // tls
     178            0 :     if let Some(tls_config) = tls_config {
     179            0 :         writeln!(file, "ssl = on")?;
     180              : 
     181              :         // postgres requires the keyfile to be in a secure file,
     182              :         // currently too complicated to ensure that at the VM level,
     183              :         // so we just copy them to another file instead. :shrug:
     184            0 :         tls::update_key_path_blocking(pgdata_path, tls_config);
     185              : 
     186              :         // these are the default, but good to be explicit.
     187            0 :         writeln!(file, "ssl_cert_file = '{SERVER_CRT}'")?;
     188            0 :         writeln!(file, "ssl_key_file = '{SERVER_KEY}'")?;
     189            0 :     }
     190              : 
     191              :     // Locales
     192            0 :     if cfg!(target_os = "macos") {
     193            0 :         writeln!(file, "lc_messages='C'")?;
     194            0 :         writeln!(file, "lc_monetary='C'")?;
     195            0 :         writeln!(file, "lc_time='C'")?;
     196            0 :         writeln!(file, "lc_numeric='C'")?;
     197              :     } else {
     198            0 :         writeln!(file, "lc_messages='C.UTF-8'")?;
     199            0 :         writeln!(file, "lc_monetary='C.UTF-8'")?;
     200            0 :         writeln!(file, "lc_time='C.UTF-8'")?;
     201            0 :         writeln!(file, "lc_numeric='C.UTF-8'")?;
     202              :     }
     203              : 
     204            0 :     writeln!(file, "neon.compute_mode={}", spec.mode.to_type_str())?;
     205            0 :     match spec.mode {
     206            0 :         ComputeMode::Primary => {}
     207            0 :         ComputeMode::Static(lsn) => {
     208              :             // hot_standby is 'on' by default, but let's be explicit
     209            0 :             writeln!(file, "hot_standby=on")?;
     210            0 :             writeln!(file, "recovery_target_lsn='{lsn}'")?;
     211              :         }
     212              :         ComputeMode::Replica => {
     213              :             // hot_standby is 'on' by default, but let's be explicit
     214            0 :             writeln!(file, "hot_standby=on")?;
     215              :         }
     216              :     }
     217              : 
     218            0 :     if cfg!(target_os = "linux") {
     219              :         // Check /proc/sys/vm/overcommit_memory -- if it equals 2 (i.e. linux memory overcommit is
     220              :         // disabled), then the control plane has enabled swap and we should set
     221              :         // dynamic_shared_memory_type = 'mmap'.
     222              :         //
     223              :         // This is (maybe?) temporary - for more, see https://github.com/neondatabase/cloud/issues/12047.
     224            0 :         let overcommit_memory_contents = std::fs::read_to_string("/proc/sys/vm/overcommit_memory")
     225              :             // ignore any errors - they may be expected to occur under certain situations (e.g. when
     226              :             // not running in Linux).
     227            0 :             .unwrap_or_else(|_| String::new());
     228            0 :         if overcommit_memory_contents.trim() == "2" {
     229            0 :             let opt = GenericOption {
     230            0 :                 name: "dynamic_shared_memory_type".to_owned(),
     231            0 :                 value: Some("mmap".to_owned()),
     232            0 :                 vartype: "enum".to_owned(),
     233            0 :             };
     234              : 
     235            0 :             writeln!(file, "{}", opt.to_pg_setting())?;
     236            0 :         }
     237            0 :     }
     238              : 
     239            0 :     writeln!(
     240            0 :         file,
     241            0 :         "neon.privileged_role_name={}",
     242            0 :         escape_conf_value(params.privileged_role_name.as_str())
     243            0 :     )?;
     244              : 
     245              :     // If there are any extra options in the 'settings' field, append those
     246            0 :     if spec.cluster.settings.is_some() {
     247            0 :         writeln!(file, "# Managed by compute_ctl: begin")?;
     248            0 :         write!(file, "{}", spec.cluster.settings.as_pg_settings())?;
     249            0 :         writeln!(file, "# Managed by compute_ctl: end")?;
     250            0 :     }
     251              : 
     252              :     // If base audit logging is enabled, configure it.
     253              :     // In this setup, the audit log will be written to the standard postgresql log.
     254              :     //
     255              :     // If compliance audit logging is enabled, configure pgaudit.
     256              :     //
     257              :     // Note, that this is called after the settings from spec are written.
     258              :     // This way we always override the settings from the spec
     259              :     // and don't allow the user or the control plane admin to change them.
     260            0 :     match spec.audit_log_level {
     261            0 :         ComputeAudit::Disabled => {}
     262              :         ComputeAudit::Log | ComputeAudit::Base => {
     263            0 :             writeln!(file, "# Managed by compute_ctl base audit settings: start")?;
     264            0 :             writeln!(file, "pgaudit.log='ddl,role'")?;
     265              :             // Disable logging of catalog queries to reduce the noise
     266            0 :             writeln!(file, "pgaudit.log_catalog=off")?;
     267              : 
     268            0 :             if let Some(libs) = spec.cluster.settings.find("shared_preload_libraries") {
     269            0 :                 let mut extra_shared_preload_libraries = String::new();
     270            0 :                 if !libs.contains("pgaudit") {
     271            0 :                     extra_shared_preload_libraries.push_str(",pgaudit");
     272            0 :                 }
     273            0 :                 writeln!(
     274            0 :                     file,
     275            0 :                     "shared_preload_libraries='{libs}{extra_shared_preload_libraries}'"
     276            0 :                 )?;
     277              :             } else {
     278              :                 // Typically, this should be unreacheable,
     279              :                 // because we always set at least some shared_preload_libraries in the spec
     280              :                 // but let's handle it explicitly anyway.
     281            0 :                 writeln!(file, "shared_preload_libraries='neon,pgaudit'")?;
     282              :             }
     283            0 :             writeln!(file, "# Managed by compute_ctl base audit settings: end")?;
     284              :         }
     285              :         ComputeAudit::Hipaa | ComputeAudit::Extended | ComputeAudit::Full => {
     286            0 :             writeln!(
     287            0 :                 file,
     288            0 :                 "# Managed by compute_ctl compliance audit settings: begin"
     289            0 :             )?;
     290              :             // Enable logging of parameters.
     291              :             // This is very verbose and may contain sensitive data.
     292            0 :             if spec.audit_log_level == ComputeAudit::Full {
     293            0 :                 writeln!(file, "pgaudit.log_parameter=on")?;
     294            0 :                 writeln!(file, "pgaudit.log='all'")?;
     295              :             } else {
     296            0 :                 writeln!(file, "pgaudit.log_parameter=off")?;
     297            0 :                 writeln!(file, "pgaudit.log='all, -misc'")?;
     298              :             }
     299              :             // Disable logging of catalog queries
     300              :             // The catalog doesn't contain sensitive data, so we don't need to audit it.
     301            0 :             writeln!(file, "pgaudit.log_catalog=off")?;
     302              :             // Set log rotation to 5 minutes
     303              :             // TODO: tune this after performance testing
     304            0 :             writeln!(file, "pgaudit.log_rotation_age=5")?;
     305              : 
     306              :             // Enable audit logs for pg_session_jwt extension
     307              :             // TODO: Consider a good approach for shipping pg_session_jwt logs to the same sink as
     308              :             // pgAudit - additional context in https://github.com/neondatabase/cloud/issues/28863
     309              :             //
     310              :             // writeln!(file, "pg_session_jwt.audit_log=on")?;
     311              : 
     312              :             // Add audit shared_preload_libraries, if they are not present.
     313              :             //
     314              :             // The caller who sets the flag is responsible for ensuring that the necessary
     315              :             // shared_preload_libraries are present in the compute image,
     316              :             // otherwise the compute start will fail.
     317            0 :             if let Some(libs) = spec.cluster.settings.find("shared_preload_libraries") {
     318            0 :                 let mut extra_shared_preload_libraries = String::new();
     319            0 :                 if !libs.contains("pgaudit") {
     320            0 :                     extra_shared_preload_libraries.push_str(",pgaudit");
     321            0 :                 }
     322            0 :                 if !libs.contains("pgauditlogtofile") {
     323            0 :                     extra_shared_preload_libraries.push_str(",pgauditlogtofile");
     324            0 :                 }
     325            0 :                 writeln!(
     326            0 :                     file,
     327            0 :                     "shared_preload_libraries='{libs}{extra_shared_preload_libraries}'"
     328            0 :                 )?;
     329              :             } else {
     330              :                 // Typically, this should be unreacheable,
     331              :                 // because we always set at least some shared_preload_libraries in the spec
     332              :                 // but let's handle it explicitly anyway.
     333            0 :                 writeln!(
     334            0 :                     file,
     335            0 :                     "shared_preload_libraries='neon,pgaudit,pgauditlogtofile'"
     336            0 :                 )?;
     337              :             }
     338            0 :             writeln!(
     339            0 :                 file,
     340            0 :                 "# Managed by compute_ctl compliance audit settings: end"
     341            0 :             )?;
     342              :         }
     343              :     }
     344              : 
     345            0 :     writeln!(file, "neon.extension_server_port={extension_server_port}")?;
     346              : 
     347            0 :     if spec.drop_subscriptions_before_start {
     348            0 :         writeln!(file, "neon.disable_logical_replication_subscribers=true")?;
     349              :     } else {
     350              :         // be explicit about the default value
     351            0 :         writeln!(file, "neon.disable_logical_replication_subscribers=false")?;
     352              :     }
     353              : 
     354              :     // We need Postgres to send logs to rsyslog so that we can forward them
     355              :     // further to customers' log aggregation systems.
     356            0 :     if spec.logs_export_host.is_some() {
     357            0 :         writeln!(file, "log_destination='stderr,syslog'")?;
     358            0 :     }
     359              : 
     360            0 :     if lakebase_mode {
     361              :         // Explicitly set the port based on the connstr, overriding any previous port setting.
     362              :         // Note: It is important that we don't specify a different port again after this.
     363            0 :         let port = postgres_port.expect("port must be present in connstr");
     364            0 :         writeln!(file, "port = {port}")?;
     365              : 
     366              :         // This is databricks specific settings.
     367              :         // This should be at the end of the file but before `compute_ctl_temp_override.conf` below
     368              :         // so that it can override any settings above.
     369              :         // `compute_ctl_temp_override.conf` is intended to override any settings above during specific operations.
     370              :         // To prevent potential breakage in the future, we keep it above `compute_ctl_temp_override.conf`.
     371            0 :         writeln!(file, "# Databricks settings start")?;
     372            0 :         if let Some(settings) = databricks_settings {
     373            0 :             writeln!(file, "{}", settings.as_pg_settings())?;
     374            0 :         }
     375            0 :         writeln!(file, "# Databricks settings end")?;
     376            0 :     }
     377              : 
     378              :     // This is essential to keep this line at the end of the file,
     379              :     // because it is intended to override any settings above.
     380            0 :     writeln!(file, "include_if_exists = 'compute_ctl_temp_override.conf'")?;
     381              : 
     382            0 :     Ok(())
     383            0 : }
     384              : 
     385            0 : pub fn with_compute_ctl_tmp_override<F>(pgdata_path: &Path, options: &str, exec: F) -> Result<()>
     386            0 : where
     387            0 :     F: FnOnce() -> Result<()>,
     388              : {
     389            0 :     let path = pgdata_path.join("compute_ctl_temp_override.conf");
     390            0 :     let mut file = File::create(path)?;
     391            0 :     write!(file, "{options}")?;
     392              : 
     393            0 :     let res = exec();
     394              : 
     395            0 :     file.set_len(0)?;
     396              : 
     397            0 :     res
     398            0 : }
        

Generated by: LCOV version 2.1-beta