pgdo/cluster.rs
1//! Create, start, introspect, stop, and destroy PostgreSQL clusters.
2
3pub mod backup;
4pub mod config;
5pub mod resource;
6
7mod error;
8
9use std::ffi::{OsStr, OsString};
10use std::os::unix::ffi::OsStrExt;
11use std::os::unix::prelude::OsStringExt;
12use std::path::{Path, PathBuf};
13use std::process::{Command, ExitStatus};
14use std::{fmt, fs, io};
15
16use postgres;
17use shell_quote::{QuoteExt, Sh};
18pub use sqlx;
19
20use crate::runtime::{
21 strategy::{Strategy, StrategyLike},
22 Runtime,
23};
24use crate::{
25 coordinate::{
26 self,
27 State::{self, *},
28 },
29 version,
30};
31pub use error::ClusterError;
32
33/// `template0` is always present in a PostgreSQL cluster.
34///
35/// This database is a template database, though it's used to a lesser extent
36/// than `template1`.
37///
38/// `template0` should never be modified so it's rare to connect to this
39/// database, even as a convenient default – see [`DATABASE_TEMPLATE1`] for an
40/// explanation as to why.
41pub static DATABASE_TEMPLATE0: &str = "template0";
42
43/// `template1` is always present in a PostgreSQL cluster.
44///
45/// This database is used as the default template for creating new databases.
46///
47/// Connecting to a database prevents other sessions from creating new databases
48/// using that database as a template; see PostgreSQL's [Template Databases][]
49/// page to learn more about this limitation. Since `template1` is the default
50/// template, connecting to this database prevents other sessions from using a
51/// plain `CREATE DATABASE` command. In other words, it may be a good idea to
52/// connect to this database _only_ when modifying it, not as a default.
53///
54/// [Template Databases]:
55/// https://www.postgresql.org/docs/current/manage-ag-templatedbs.html
56pub static DATABASE_TEMPLATE1: &str = "template0";
57
58/// `postgres` is always created by `initdb` when building a PostgreSQL cluster.
59///
60/// From `initdb(1)`:
61/// > The postgres database is a default database meant for use by users,
62/// > utilities and third party applications.
63///
64/// Given that it can be problematic to connect to `template0` and `template1` –
65/// see [`DATABASE_TEMPLATE1`] for an explanation – `postgres` is a convenient
66/// default, hence this library uses `postgres` as the database from which to
67/// perform administrative tasks, for example.
68///
69/// Unfortunately, `postgres` can be dropped, in which case some of the
70/// functionality of this crate will be broken. Ideally we could connect to a
71/// PostgreSQL cluster without specifying a database, but that is presently not
72/// possible.
73pub static DATABASE_POSTGRES: &str = "postgres";
74
75#[derive(Debug, PartialEq, Eq, Clone)]
76pub enum ClusterStatus {
77 Running,
78 Stopped,
79 Missing,
80}
81
82impl fmt::Display for ClusterStatus {
83 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84 match self {
85 ClusterStatus::Running => write!(f, "running"),
86 ClusterStatus::Stopped => write!(f, "stopped"),
87 ClusterStatus::Missing => write!(f, "missing"),
88 }
89 }
90}
91
92/// Representation of a PostgreSQL cluster.
93///
94/// The cluster may not yet exist on disk. It may exist but be stopped, or it
95/// may be running. The methods here can be used to create, start, introspect,
96/// stop, and destroy the cluster. There's no protection against concurrent
97/// changes to the cluster made by other processes, but the functions in the
98/// [`coordinate`][`crate::coordinate`] module may help.
99#[derive(Debug)]
100pub struct Cluster {
101 /// The data directory of the cluster.
102 ///
103 /// Corresponds to the `PGDATA` environment variable.
104 pub datadir: PathBuf,
105 /// How to select the PostgreSQL installation to use with this cluster.
106 pub strategy: Strategy,
107}
108
109impl Cluster {
110 /// Represent a cluster at the given path.
111 pub fn new<P: AsRef<Path>, S: Into<Strategy>>(
112 datadir: P,
113 strategy: S,
114 ) -> Result<Self, ClusterError> {
115 Ok(Self {
116 datadir: datadir.as_ref().to_owned(),
117 strategy: strategy.into(),
118 })
119 }
120
121 /// Determine the runtime to use with this cluster.
122 fn runtime(&self) -> Result<Runtime, ClusterError> {
123 match version(self)? {
124 None => self
125 .strategy
126 .fallback()
127 .ok_or_else(|| ClusterError::RuntimeDefaultNotFound),
128 Some(version) => self
129 .strategy
130 .select(&version.into())
131 .ok_or_else(|| ClusterError::RuntimeNotFound(version)),
132 }
133 }
134
135 /// Return a [`Command`] that will invoke `pg_ctl` with the environment
136 /// referring to this cluster.
137 fn ctl(&self) -> Result<Command, ClusterError> {
138 let mut command = self.runtime()?.execute("pg_ctl");
139 command.env("PGDATA", &self.datadir);
140 command.env("PGHOST", &self.datadir);
141 Ok(command)
142 }
143
144 /// Check if this cluster is running.
145 ///
146 /// Convenient call-through to [`status`][`Self::status`]; only returns
147 /// `true` when the cluster is definitely running.
148 pub fn running(&self) -> Result<bool, ClusterError> {
149 self.status().map(|status| status == ClusterStatus::Running)
150 }
151
152 /// Check the status of this cluster.
153 ///
154 /// Tries to distinguish carefully between "definitely running", "definitely
155 /// not running", "missing", and "don't know". The latter results in
156 /// [`ClusterError`].
157 pub fn status(&self) -> Result<ClusterStatus, ClusterError> {
158 let output = self.ctl()?.arg("status").output()?;
159 let code = match output.status.code() {
160 // Killed by signal; return early.
161 None => return Err(ClusterError::CommandError(output)),
162 // Success; return early (the server is running).
163 Some(0) => return Ok(ClusterStatus::Running),
164 // More work required to decode what this means.
165 Some(code) => code,
166 };
167 let runtime = self.runtime()?;
168 // PostgreSQL has evolved to return different error codes in
169 // later versions, so here we check for specific codes to avoid
170 // masking errors from insufficient permissions or missing
171 // executables, for example.
172 let status = match runtime.version {
173 // PostgreSQL 10.x and later.
174 version::Version::Post10(_major, _minor) => {
175 // PostgreSQL 10
176 // https://www.postgresql.org/docs/10/static/app-pg-ctl.html
177 match code {
178 // 3 means that the data directory is present and
179 // accessible but that the server is not running.
180 3 => Some(ClusterStatus::Stopped),
181 // 4 means that the data directory is not present or is
182 // not accessible. If it's missing, then the server is
183 // not running. If it is present but not accessible
184 // then crash because we can't know if the server is
185 // running or not.
186 4 if !exists(self) => Some(ClusterStatus::Missing),
187 // For anything else we don't know.
188 _ => None,
189 }
190 }
191 // PostgreSQL 9.x only.
192 version::Version::Pre10(9, point, _minor) => {
193 // PostgreSQL 9.4+
194 // https://www.postgresql.org/docs/9.4/static/app-pg-ctl.html
195 // https://www.postgresql.org/docs/9.5/static/app-pg-ctl.html
196 // https://www.postgresql.org/docs/9.6/static/app-pg-ctl.html
197 if point >= 4 {
198 match code {
199 // 3 means that the data directory is present and
200 // accessible but that the server is not running.
201 3 => Some(ClusterStatus::Stopped),
202 // 4 means that the data directory is not present or is
203 // not accessible. If it's missing, then the server is
204 // not running. If it is present but not accessible
205 // then crash because we can't know if the server is
206 // running or not.
207 4 if !exists(self) => Some(ClusterStatus::Missing),
208 // For anything else we don't know.
209 _ => None,
210 }
211 }
212 // PostgreSQL 9.2+
213 // https://www.postgresql.org/docs/9.2/static/app-pg-ctl.html
214 // https://www.postgresql.org/docs/9.3/static/app-pg-ctl.html
215 else if point >= 2 {
216 match code {
217 // 3 means that the data directory is present and
218 // accessible but that the server is not running OR
219 // that the data directory is not present.
220 3 if !exists(self) => Some(ClusterStatus::Missing),
221 3 => Some(ClusterStatus::Stopped),
222 // For anything else we don't know.
223 _ => None,
224 }
225 }
226 // PostgreSQL 9.0+
227 // https://www.postgresql.org/docs/9.0/static/app-pg-ctl.html
228 // https://www.postgresql.org/docs/9.1/static/app-pg-ctl.html
229 else {
230 match code {
231 // 1 means that the server is not running OR the data
232 // directory is not present OR that the data directory
233 // is not accessible.
234 1 if !exists(self) => Some(ClusterStatus::Missing),
235 1 => Some(ClusterStatus::Stopped),
236 // For anything else we don't know.
237 _ => None,
238 }
239 }
240 }
241 // All other versions.
242 version::Version::Pre10(_major, _point, _minor) => None,
243 };
244
245 match status {
246 Some(running) => Ok(running),
247 // TODO: Perhaps include the exit code from `pg_ctl status` in the
248 // error message, and whatever it printed out.
249 None => Err(ClusterError::UnsupportedVersion(runtime.version)),
250 }
251 }
252
253 /// Return the path to the PID file used in this cluster.
254 ///
255 /// The PID file does not necessarily exist.
256 pub fn pidfile(&self) -> PathBuf {
257 self.datadir.join("postmaster.pid")
258 }
259
260 /// Return the path to the log file used in this cluster.
261 ///
262 /// The log file does not necessarily exist.
263 pub fn logfile(&self) -> PathBuf {
264 self.datadir.join("postmaster.log")
265 }
266
267 /// Create the cluster if it does not already exist.
268 pub fn create(&self) -> Result<State, ClusterError> {
269 if exists(self) {
270 // Nothing more to do; the cluster is already in place.
271 Ok(Unmodified)
272 } else {
273 // Create the cluster and report back that we did so.
274 fs::create_dir_all(&self.datadir)?;
275 #[allow(clippy::suspicious_command_arg_space)]
276 self.ctl()?
277 .arg("init")
278 .arg("-s")
279 .arg("-o")
280 // Passing multiple flags in a single `arg(...)` is
281 // intentional. These constitute the single value for the
282 // `-o` flag above.
283 .arg("-E utf8 --locale C -A trust")
284 .env("TZ", "UTC")
285 .output()?;
286 Ok(Modified)
287 }
288 }
289
290 /// Start the cluster if it's not already running, with the given options.
291 ///
292 /// Returns [`State::Unmodified`] if the cluster is already running, meaning
293 /// the given options were **NOT** applied.
294 pub fn start(
295 &self,
296 options: &[(config::Parameter, config::Value)],
297 ) -> Result<State, ClusterError> {
298 // Ensure that the cluster has been created.
299 self.create()?;
300 // Check if we're running already.
301 if self.running()? {
302 // We didn't start this cluster; say so.
303 return Ok(Unmodified);
304 }
305 // Construct the options that `pg_ctl` will pass through to `postgres`.
306 // These have to be carefully escaped for the target shell – which is
307 // likely to be `sh`. Here's what they mean:
308 // -h <arg> -- host name; empty arg means Unix socket only.
309 // -k -- socket directory.
310 // -c name=value -- set a configuration parameter.
311 let options = {
312 let mut arg: Vec<u8> = b"-h '' -k ".into();
313 arg.push_quoted(Sh, &self.datadir);
314 for (parameter, value) in options {
315 arg.extend(b" -c ");
316 arg.push_quoted(Sh, &format!("{parameter}={value}",));
317 }
318 OsString::from_vec(arg)
319 };
320 // Next, invoke `pg_ctl` to start the cluster.
321 // -l <file> -- log file.
322 // -s -- no informational messages.
323 // -w -- wait until startup is complete.
324 // -o <string> -- options to pass through to `postgres`.
325 self.ctl()?
326 .arg("start")
327 .arg("-l")
328 .arg(self.logfile())
329 .arg("-s")
330 .arg("-w")
331 .arg("-o")
332 .arg(options)
333 .output()?;
334 // We did actually start the cluster; say so.
335 Ok(Modified)
336 }
337
338 /// Connect to this cluster.
339 ///
340 /// When the database is not specified, connects to [`DATABASE_POSTGRES`].
341 fn connect(&self, database: Option<&str>) -> Result<postgres::Client, ClusterError> {
342 let user = crate::util::current_user()?;
343 let host = self.datadir.to_string_lossy(); // postgres crate API limitation.
344 let client = postgres::Client::configure()
345 .host(&host)
346 .dbname(database.unwrap_or(DATABASE_POSTGRES))
347 .user(&user)
348 .connect(postgres::NoTls)?;
349 Ok(client)
350 }
351
352 /// Create a lazy SQLx pool for this cluster.
353 ///
354 /// Although it's possible to call this anywhere, at runtime it needs a
355 /// Tokio context to work, e.g.:
356 ///
357 /// ```rust,no_run
358 /// # use pgdo::cluster::ClusterError;
359 /// # let runtime = pgdo::runtime::strategy::Strategy::default();
360 /// # let cluster = pgdo::cluster::Cluster::new("some/where", runtime)?;
361 /// let tokio = tokio::runtime::Runtime::new()?;
362 /// let rows = tokio.block_on(async {
363 /// let pool = cluster.pool(None)?;
364 /// let rows = sqlx::query("SELECT 1").fetch_all(&pool).await?;
365 /// Ok::<_, ClusterError>(rows)
366 /// })?;
367 /// # Ok::<(), ClusterError>(())
368 /// ```
369 ///
370 /// When the database is not specified, connects to [`DATABASE_POSTGRES`].
371 pub fn pool(&self, database: Option<&str>) -> Result<sqlx::PgPool, ClusterError> {
372 Ok(sqlx::PgPool::connect_lazy_with(
373 sqlx::postgres::PgConnectOptions::new()
374 .socket(&self.datadir)
375 .database(database.unwrap_or(DATABASE_POSTGRES))
376 .username(&crate::util::current_user()?)
377 .application_name("pgdo"),
378 ))
379 }
380
381 /// Return a URL for this cluster, if possible.
382 ///
383 /// It is not possible to return a URL for a cluster when `self.datadir` is
384 /// not valid UTF-8, in which case `Ok(None)` is returned.
385 fn url(&self, database: &str) -> Result<Option<url::Url>, url::ParseError> {
386 match self.datadir.to_str() {
387 Some(datadir) => url::Url::parse_with_params(
388 "postgresql://",
389 [("host", datadir), ("dbname", database)],
390 )
391 .map(Some),
392 None => Ok(None),
393 }
394 }
395
396 /// Run `psql` against this cluster, in the given database.
397 ///
398 /// When the database is not specified, connects to [`DATABASE_POSTGRES`].
399 pub fn shell(&self, database: Option<&str>) -> Result<ExitStatus, ClusterError> {
400 let mut command = self.runtime()?.execute("psql");
401 self.set_env(command.arg("--quiet"), database)?;
402 Ok(command.spawn()?.wait()?)
403 }
404
405 /// Run the given command against this cluster.
406 ///
407 /// The command is run with the `PGDATA`, `PGHOST`, and `PGDATABASE`
408 /// environment variables set appropriately.
409 ///
410 /// When the database is not specified, uses [`DATABASE_POSTGRES`].
411 pub fn exec<T: AsRef<OsStr>>(
412 &self,
413 database: Option<&str>,
414 command: T,
415 args: &[T],
416 ) -> Result<ExitStatus, ClusterError> {
417 let mut command = self.runtime()?.command(command);
418 self.set_env(command.args(args), database)?;
419 Ok(command.spawn()?.wait()?)
420 }
421
422 /// Set the environment variables for this cluster.
423 fn set_env(&self, command: &mut Command, database: Option<&str>) -> Result<(), ClusterError> {
424 let database = database.unwrap_or(DATABASE_POSTGRES);
425
426 // Set a few standard PostgreSQL environment variables.
427 command.env("PGDATA", &self.datadir);
428 command.env("PGHOST", &self.datadir);
429 command.env("PGDATABASE", database);
430
431 // Set `DATABASE_URL` if `self.datadir` is valid UTF-8, otherwise ensure
432 // that `DATABASE_URL` is erased from the command's environment.
433 match self.url(database)? {
434 Some(url) => command.env("DATABASE_URL", url.as_str()),
435 None => command.env_remove("DATABASE_URL"),
436 };
437
438 Ok(())
439 }
440
441 /// The names of databases in this cluster.
442 pub fn databases(&self) -> Result<Vec<String>, ClusterError> {
443 let mut conn = self.connect(None)?;
444 let rows = conn.query(
445 "SELECT datname FROM pg_catalog.pg_database ORDER BY datname",
446 &[],
447 )?;
448 let datnames: Vec<String> = rows.iter().map(|row| row.get(0)).collect();
449 Ok(datnames)
450 }
451
452 /// Create the named database.
453 ///
454 /// Returns [`Unmodified`] if the database already exists, otherwise it
455 /// returns [`Modified`].
456 pub fn createdb(&self, database: &str) -> Result<State, ClusterError> {
457 use postgres::error::SqlState;
458 let statement = format!(
459 "CREATE DATABASE {}",
460 postgres_protocol::escape::escape_identifier(database)
461 );
462 match self.connect(None)?.execute(statement.as_str(), &[]) {
463 Err(err) if err.code() == Some(&SqlState::DUPLICATE_DATABASE) => Ok(Unmodified),
464 Err(err) => Err(err)?,
465 Ok(_) => Ok(Modified),
466 }
467 }
468
469 /// Drop the named database.
470 ///
471 /// Returns [`Unmodified`] if the database does not exist, otherwise it
472 /// returns [`Modified`].
473 pub fn dropdb(&self, database: &str) -> Result<State, ClusterError> {
474 use postgres::error::SqlState;
475 let statement = format!(
476 "DROP DATABASE {}",
477 postgres_protocol::escape::escape_identifier(database)
478 );
479 match self.connect(None)?.execute(statement.as_str(), &[]) {
480 Err(err) if err.code() == Some(&SqlState::UNDEFINED_DATABASE) => Ok(Unmodified),
481 Err(err) => Err(err)?,
482 Ok(_) => Ok(Modified),
483 }
484 }
485
486 /// Stop the cluster if it's running.
487 pub fn stop(&self) -> Result<State, ClusterError> {
488 // If the cluster's not already running, don't do anything.
489 if !self.running()? {
490 return Ok(Unmodified);
491 }
492 // pg_ctl options:
493 // -w -- wait for shutdown to complete.
494 // -m <mode> -- shutdown mode.
495 self.ctl()?
496 .arg("stop")
497 .arg("-s")
498 .arg("-w")
499 .arg("-m")
500 .arg("fast")
501 .output()?;
502 Ok(Modified)
503 }
504
505 /// Destroy the cluster if it exists, after stopping it.
506 pub fn destroy(&self) -> Result<State, ClusterError> {
507 self.stop()?;
508 match fs::remove_dir_all(&self.datadir) {
509 Ok(()) => Ok(Modified),
510 Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(Unmodified),
511 Err(err) => Err(err)?,
512 }
513 }
514}
515
516impl AsRef<Path> for Cluster {
517 fn as_ref(&self) -> &Path {
518 &self.datadir
519 }
520}
521
522/// A fairly simplistic but quick check: does the directory exist and does it
523/// look like a PostgreSQL cluster data directory, i.e. does it contain a file
524/// named `PG_VERSION`?
525///
526/// [`version()`] provides a more reliable measure, plus yields the version of
527/// PostgreSQL required to use the cluster.
528pub fn exists<P: AsRef<Path>>(datadir: P) -> bool {
529 let datadir = datadir.as_ref();
530 datadir.is_dir() && datadir.join("PG_VERSION").is_file()
531}
532
533/// Yields the version of PostgreSQL required to use a cluster.
534///
535/// This returns the version from the file named `PG_VERSION` in the data
536/// directory if it exists, otherwise this returns `None`. For PostgreSQL
537/// versions before 10 this is typically (maybe always) the major and point
538/// version, e.g. 9.4 rather than 9.4.26. For version 10 and above it appears to
539/// be just the major number, e.g. 14 rather than 14.2.
540pub fn version<P: AsRef<Path>>(
541 datadir: P,
542) -> Result<Option<version::PartialVersion>, ClusterError> {
543 let version_file = datadir.as_ref().join("PG_VERSION");
544 match std::fs::read_to_string(version_file) {
545 Ok(version) => Ok(Some(version.parse()?)),
546 Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(None),
547 Err(err) => Err(err)?,
548 }
549}
550
551/// Determine the names of superuser roles in a cluster (that can log in).
552///
553/// It may not be possible to even connect to a running cluster when you don't
554/// know a role to use.
555///
556/// This gets around the problem by launching the cluster in single-user mode
557/// and matching the output of a single query of the `pg_roles` table. It's
558/// hacky and fragile but it may work for you.
559///
560/// If no superusers are found, this returns an error containing the output from
561/// the `postgres` process.
562///
563/// # Panics
564///
565/// This function panics if the regular expression used to match the output does
566/// not compile; that's a bug and should never occur in a release build.
567///
568/// It can also panic if the thread that writes to the single-user `postgres`
569/// process itself panics, but under normal circumstances that also should never
570/// happen.
571///
572pub fn determine_superuser_role_names(
573 cluster: &Cluster,
574) -> Result<std::collections::HashSet<String>, ClusterError> {
575 use regex::Regex;
576 use std::io::Write;
577 use std::panic::panic_any;
578 use std::process::Stdio;
579 use std::sync::LazyLock;
580
581 static QUERY: &[u8] = b"select rolname from pg_roles where rolsuper and rolcanlogin\n";
582 static RE: LazyLock<Regex> = LazyLock::new(|| {
583 Regex::new(r#"\brolname\s*=\s*"(.+)""#)
584 .expect("invalid regex (for matching single-user role names)")
585 });
586
587 let mut child = cluster
588 .runtime()?
589 .execute("postgres")
590 .arg("--single")
591 .arg("-D")
592 .arg(&cluster.datadir)
593 .arg("postgres")
594 .stdin(Stdio::piped())
595 .stdout(Stdio::piped())
596 .stderr(Stdio::piped())
597 .spawn()?;
598
599 let mut stdin = child.stdin.take().expect("could not take stdin");
600 let writer = std::thread::spawn(move || stdin.write_all(QUERY));
601 let output = child.wait_with_output()?;
602 let stdout = String::from_utf8_lossy(&output.stdout);
603 let superusers: std::collections::HashSet<_> = RE
604 .captures_iter(&stdout)
605 .filter_map(|capture| capture.get(1))
606 .map(|m| m.as_str().to_owned())
607 .collect();
608
609 match writer.join() {
610 Err(err) => panic_any(err),
611 Ok(result) => result?,
612 }
613
614 if superusers.is_empty() {
615 return Err(ClusterError::CommandError(output));
616 }
617
618 Ok(superusers)
619}
620
621pub type Options<'a> = &'a [(config::Parameter<'a>, config::Value)];
622
623/// [`Cluster`] can be coordinated.
624impl coordinate::Subject for Cluster {
625 type Error = ClusterError;
626 type Options<'a> = Options<'a>;
627
628 fn start(&self, options: Self::Options<'_>) -> Result<State, Self::Error> {
629 self.start(options)
630 }
631
632 fn stop(&self) -> Result<State, Self::Error> {
633 self.stop()
634 }
635
636 fn destroy(&self) -> Result<State, Self::Error> {
637 self.destroy()
638 }
639
640 fn exists(&self) -> Result<bool, Self::Error> {
641 Ok(exists(self))
642 }
643
644 fn running(&self) -> Result<bool, Self::Error> {
645 self.running()
646 }
647}
648
649#[allow(clippy::unreadable_literal)]
650const UUID_NS: uuid::Uuid = uuid::Uuid::from_u128(93875103436633470414348750305797058811);
651
652pub type ClusterGuard = coordinate::guard::Guard<Cluster>;
653
654/// Create and start a cluster at the given path, with the given options.
655///
656/// Uses the default runtime strategy. Returns a guard which will stop the
657/// cluster when it's dropped.
658pub fn run<P: AsRef<Path>>(
659 path: P,
660 options: Options<'_>,
661) -> Result<ClusterGuard, coordinate::CoordinateError<ClusterError>> {
662 let path = path.as_ref();
663 // We have to create the data directory so that we can canonicalize its
664 // location. This is because we use the data directory's path as the basis
665 // for the lock file's name. This is duplicative – `Cluster::create` also
666 // creates the data directory – but necessary.
667 fs::create_dir_all(path)?;
668 let path = path.canonicalize()?;
669
670 let strategy = crate::runtime::strategy::Strategy::default();
671 let cluster = crate::cluster::Cluster::new(&path, strategy)?;
672
673 let lock_name = path.as_os_str().as_bytes();
674 let lock_uuid = uuid::Uuid::new_v5(&UUID_NS, lock_name);
675 let lock = crate::lock::UnlockedFile::try_from(&lock_uuid)?;
676
677 ClusterGuard::startup(lock, cluster, options)
678}