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