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::{ComputeAudit, ComputeMode, ComputeSpec, GenericOption};
11 :
12 : use crate::compute::ComputeNodeParams;
13 : use crate::pg_helpers::{
14 : GenericOptionExt, GenericOptionsSearch, PgOptionsSerialize, escape_conf_value,
15 : };
16 : use crate::tls::{self, SERVER_CRT, SERVER_KEY};
17 :
18 : /// Check that `line` is inside a text file and put it there if it is not.
19 : /// Create file if it doesn't exist.
20 3 : pub fn line_in_file(path: &Path, line: &str) -> Result<bool> {
21 3 : let mut file = OpenOptions::new()
22 3 : .read(true)
23 3 : .write(true)
24 3 : .create(true)
25 3 : .append(false)
26 3 : .truncate(false)
27 3 : .open(path)?;
28 3 : let buf = io::BufReader::new(&file);
29 3 : let mut count: usize = 0;
30 :
31 5 : for l in buf.lines() {
32 5 : if l? == line {
33 1 : return Ok(false);
34 4 : }
35 4 : count = 1;
36 : }
37 :
38 2 : write!(file, "{}{}", "\n".repeat(count), line)?;
39 2 : Ok(true)
40 3 : }
41 :
42 : /// Create or completely rewrite configuration file specified by `path`
43 0 : pub fn write_postgres_conf(
44 0 : pgdata_path: &Path,
45 0 : params: &ComputeNodeParams,
46 0 : spec: &ComputeSpec,
47 0 : extension_server_port: u16,
48 0 : tls_config: &Option<TlsConfig>,
49 0 : ) -> Result<()> {
50 0 : let path = pgdata_path.join("postgresql.conf");
51 : // File::create() destroys the file content if it exists.
52 0 : let mut file = File::create(path)?;
53 :
54 : // Write the postgresql.conf content from the spec file as is.
55 0 : if let Some(conf) = &spec.cluster.postgresql_conf {
56 0 : writeln!(file, "{conf}")?;
57 0 : }
58 :
59 : // Stripe size GUC should be defined prior to connection string
60 0 : if let Some(stripe_size) = spec.shard_stripe_size {
61 0 : writeln!(file, "neon.stripe_size={stripe_size}")?;
62 0 : }
63 : // Add options for connecting to storage
64 0 : writeln!(file, "# Neon storage settings")?;
65 0 : if let Some(s) = &spec.pageserver_connstring {
66 0 : writeln!(file, "neon.pageserver_connstring={}", escape_conf_value(s))?;
67 0 : }
68 0 : if !spec.safekeeper_connstrings.is_empty() {
69 0 : let mut neon_safekeepers_value = String::new();
70 0 : tracing::info!(
71 0 : "safekeepers_connstrings is not zero, gen: {:?}",
72 : spec.safekeepers_generation
73 : );
74 : // If generation is given, prepend sk list with g#number:
75 0 : if let Some(generation) = spec.safekeepers_generation {
76 0 : write!(neon_safekeepers_value, "g#{generation}:")?;
77 0 : }
78 0 : neon_safekeepers_value.push_str(&spec.safekeeper_connstrings.join(","));
79 0 : writeln!(
80 0 : file,
81 0 : "neon.safekeepers={}",
82 0 : escape_conf_value(&neon_safekeepers_value)
83 0 : )?;
84 0 : }
85 0 : if let Some(s) = &spec.tenant_id {
86 0 : writeln!(file, "neon.tenant_id={}", escape_conf_value(&s.to_string()))?;
87 0 : }
88 0 : if let Some(s) = &spec.timeline_id {
89 0 : writeln!(
90 0 : file,
91 0 : "neon.timeline_id={}",
92 0 : escape_conf_value(&s.to_string())
93 0 : )?;
94 0 : }
95 0 : if let Some(s) = &spec.project_id {
96 0 : writeln!(file, "neon.project_id={}", escape_conf_value(s))?;
97 0 : }
98 0 : if let Some(s) = &spec.branch_id {
99 0 : writeln!(file, "neon.branch_id={}", escape_conf_value(s))?;
100 0 : }
101 0 : if let Some(s) = &spec.endpoint_id {
102 0 : writeln!(file, "neon.endpoint_id={}", escape_conf_value(s))?;
103 0 : }
104 :
105 : // tls
106 0 : if let Some(tls_config) = tls_config {
107 0 : writeln!(file, "ssl = on")?;
108 :
109 : // postgres requires the keyfile to be in a secure file,
110 : // currently too complicated to ensure that at the VM level,
111 : // so we just copy them to another file instead. :shrug:
112 0 : tls::update_key_path_blocking(pgdata_path, tls_config);
113 :
114 : // these are the default, but good to be explicit.
115 0 : writeln!(file, "ssl_cert_file = '{SERVER_CRT}'")?;
116 0 : writeln!(file, "ssl_key_file = '{SERVER_KEY}'")?;
117 0 : }
118 :
119 : // Locales
120 0 : if cfg!(target_os = "macos") {
121 0 : writeln!(file, "lc_messages='C'")?;
122 0 : writeln!(file, "lc_monetary='C'")?;
123 0 : writeln!(file, "lc_time='C'")?;
124 0 : writeln!(file, "lc_numeric='C'")?;
125 : } else {
126 0 : writeln!(file, "lc_messages='C.UTF-8'")?;
127 0 : writeln!(file, "lc_monetary='C.UTF-8'")?;
128 0 : writeln!(file, "lc_time='C.UTF-8'")?;
129 0 : writeln!(file, "lc_numeric='C.UTF-8'")?;
130 : }
131 :
132 0 : writeln!(file, "neon.compute_mode={}", spec.mode.to_type_str())?;
133 0 : match spec.mode {
134 0 : ComputeMode::Primary => {}
135 0 : ComputeMode::Static(lsn) => {
136 : // hot_standby is 'on' by default, but let's be explicit
137 0 : writeln!(file, "hot_standby=on")?;
138 0 : writeln!(file, "recovery_target_lsn='{lsn}'")?;
139 : }
140 : ComputeMode::Replica => {
141 : // hot_standby is 'on' by default, but let's be explicit
142 0 : writeln!(file, "hot_standby=on")?;
143 : }
144 : }
145 :
146 0 : if cfg!(target_os = "linux") {
147 : // Check /proc/sys/vm/overcommit_memory -- if it equals 2 (i.e. linux memory overcommit is
148 : // disabled), then the control plane has enabled swap and we should set
149 : // dynamic_shared_memory_type = 'mmap'.
150 : //
151 : // This is (maybe?) temporary - for more, see https://github.com/neondatabase/cloud/issues/12047.
152 0 : let overcommit_memory_contents = std::fs::read_to_string("/proc/sys/vm/overcommit_memory")
153 : // ignore any errors - they may be expected to occur under certain situations (e.g. when
154 : // not running in Linux).
155 0 : .unwrap_or_else(|_| String::new());
156 0 : if overcommit_memory_contents.trim() == "2" {
157 0 : let opt = GenericOption {
158 0 : name: "dynamic_shared_memory_type".to_owned(),
159 0 : value: Some("mmap".to_owned()),
160 0 : vartype: "enum".to_owned(),
161 0 : };
162 :
163 0 : writeln!(file, "{}", opt.to_pg_setting())?;
164 0 : }
165 0 : }
166 :
167 0 : writeln!(
168 0 : file,
169 0 : "neon.privileged_role_name={}",
170 0 : escape_conf_value(params.privileged_role_name.as_str())
171 0 : )?;
172 :
173 : // If there are any extra options in the 'settings' field, append those
174 0 : if spec.cluster.settings.is_some() {
175 0 : writeln!(file, "# Managed by compute_ctl: begin")?;
176 0 : write!(file, "{}", spec.cluster.settings.as_pg_settings())?;
177 0 : writeln!(file, "# Managed by compute_ctl: end")?;
178 0 : }
179 :
180 : // If base audit logging is enabled, configure it.
181 : // In this setup, the audit log will be written to the standard postgresql log.
182 : //
183 : // If compliance audit logging is enabled, configure pgaudit.
184 : //
185 : // Note, that this is called after the settings from spec are written.
186 : // This way we always override the settings from the spec
187 : // and don't allow the user or the control plane admin to change them.
188 0 : match spec.audit_log_level {
189 0 : ComputeAudit::Disabled => {}
190 : ComputeAudit::Log | ComputeAudit::Base => {
191 0 : writeln!(file, "# Managed by compute_ctl base audit settings: start")?;
192 0 : writeln!(file, "pgaudit.log='ddl,role'")?;
193 : // Disable logging of catalog queries to reduce the noise
194 0 : writeln!(file, "pgaudit.log_catalog=off")?;
195 :
196 0 : if let Some(libs) = spec.cluster.settings.find("shared_preload_libraries") {
197 0 : let mut extra_shared_preload_libraries = String::new();
198 0 : if !libs.contains("pgaudit") {
199 0 : extra_shared_preload_libraries.push_str(",pgaudit");
200 0 : }
201 0 : writeln!(
202 0 : file,
203 0 : "shared_preload_libraries='{libs}{extra_shared_preload_libraries}'"
204 0 : )?;
205 : } else {
206 : // Typically, this should be unreacheable,
207 : // because we always set at least some shared_preload_libraries in the spec
208 : // but let's handle it explicitly anyway.
209 0 : writeln!(file, "shared_preload_libraries='neon,pgaudit'")?;
210 : }
211 0 : writeln!(file, "# Managed by compute_ctl base audit settings: end")?;
212 : }
213 : ComputeAudit::Hipaa | ComputeAudit::Extended | ComputeAudit::Full => {
214 0 : writeln!(
215 0 : file,
216 0 : "# Managed by compute_ctl compliance audit settings: begin"
217 0 : )?;
218 : // Enable logging of parameters.
219 : // This is very verbose and may contain sensitive data.
220 0 : if spec.audit_log_level == ComputeAudit::Full {
221 0 : writeln!(file, "pgaudit.log_parameter=on")?;
222 0 : writeln!(file, "pgaudit.log='all'")?;
223 : } else {
224 0 : writeln!(file, "pgaudit.log_parameter=off")?;
225 0 : writeln!(file, "pgaudit.log='all, -misc'")?;
226 : }
227 : // Disable logging of catalog queries
228 : // The catalog doesn't contain sensitive data, so we don't need to audit it.
229 0 : writeln!(file, "pgaudit.log_catalog=off")?;
230 : // Set log rotation to 5 minutes
231 : // TODO: tune this after performance testing
232 0 : writeln!(file, "pgaudit.log_rotation_age=5")?;
233 :
234 : // Enable audit logs for pg_session_jwt extension
235 : // TODO: Consider a good approach for shipping pg_session_jwt logs to the same sink as
236 : // pgAudit - additional context in https://github.com/neondatabase/cloud/issues/28863
237 : //
238 : // writeln!(file, "pg_session_jwt.audit_log=on")?;
239 :
240 : // Add audit shared_preload_libraries, if they are not present.
241 : //
242 : // The caller who sets the flag is responsible for ensuring that the necessary
243 : // shared_preload_libraries are present in the compute image,
244 : // otherwise the compute start will fail.
245 0 : if let Some(libs) = spec.cluster.settings.find("shared_preload_libraries") {
246 0 : let mut extra_shared_preload_libraries = String::new();
247 0 : if !libs.contains("pgaudit") {
248 0 : extra_shared_preload_libraries.push_str(",pgaudit");
249 0 : }
250 0 : if !libs.contains("pgauditlogtofile") {
251 0 : extra_shared_preload_libraries.push_str(",pgauditlogtofile");
252 0 : }
253 0 : writeln!(
254 0 : file,
255 0 : "shared_preload_libraries='{libs}{extra_shared_preload_libraries}'"
256 0 : )?;
257 : } else {
258 : // Typically, this should be unreacheable,
259 : // because we always set at least some shared_preload_libraries in the spec
260 : // but let's handle it explicitly anyway.
261 0 : writeln!(
262 0 : file,
263 0 : "shared_preload_libraries='neon,pgaudit,pgauditlogtofile'"
264 0 : )?;
265 : }
266 0 : writeln!(
267 0 : file,
268 0 : "# Managed by compute_ctl compliance audit settings: end"
269 0 : )?;
270 : }
271 : }
272 :
273 0 : writeln!(file, "neon.extension_server_port={extension_server_port}")?;
274 :
275 0 : if spec.drop_subscriptions_before_start {
276 0 : writeln!(file, "neon.disable_logical_replication_subscribers=true")?;
277 : } else {
278 : // be explicit about the default value
279 0 : writeln!(file, "neon.disable_logical_replication_subscribers=false")?;
280 : }
281 :
282 : // We need Postgres to send logs to rsyslog so that we can forward them
283 : // further to customers' log aggregation systems.
284 0 : if spec.logs_export_host.is_some() {
285 0 : writeln!(file, "log_destination='stderr,syslog'")?;
286 0 : }
287 :
288 : // This is essential to keep this line at the end of the file,
289 : // because it is intended to override any settings above.
290 0 : writeln!(file, "include_if_exists = 'compute_ctl_temp_override.conf'")?;
291 :
292 0 : Ok(())
293 0 : }
294 :
295 0 : pub fn with_compute_ctl_tmp_override<F>(pgdata_path: &Path, options: &str, exec: F) -> Result<()>
296 0 : where
297 0 : F: FnOnce() -> Result<()>,
298 : {
299 0 : let path = pgdata_path.join("compute_ctl_temp_override.conf");
300 0 : let mut file = File::create(path)?;
301 0 : write!(file, "{options}")?;
302 :
303 0 : let res = exec();
304 :
305 0 : file.set_len(0)?;
306 :
307 0 : res
308 0 : }
|