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
580    static QUERY: &[u8] = b"select rolname from pg_roles where rolsuper and rolcanlogin\n";
581    lazy_static! {
582        static ref RE: Regex = Regex::new(r#"\brolname\s*=\s*"(.+)""#)
583            .expect("invalid regex (for matching single-user role names)");
584    }
585
586    let mut child = cluster
587        .runtime()?
588        .execute("postgres")
589        .arg("--single")
590        .arg("-D")
591        .arg(&cluster.datadir)
592        .arg("postgres")
593        .stdin(Stdio::piped())
594        .stdout(Stdio::piped())
595        .stderr(Stdio::piped())
596        .spawn()?;
597
598    let mut stdin = child.stdin.take().expect("could not take stdin");
599    let writer = std::thread::spawn(move || stdin.write_all(QUERY));
600    let output = child.wait_with_output()?;
601    let stdout = String::from_utf8_lossy(&output.stdout);
602    let superusers: std::collections::HashSet<_> = RE
603        .captures_iter(&stdout)
604        .filter_map(|capture| capture.get(1))
605        .map(|m| m.as_str().to_owned())
606        .collect();
607
608    match writer.join() {
609        Err(err) => panic_any(err),
610        Ok(result) => result?,
611    }
612
613    if superusers.is_empty() {
614        return Err(ClusterError::CommandError(output));
615    }
616
617    Ok(superusers)
618}
619
620pub type Options<'a> = &'a [(config::Parameter<'a>, config::Value)];
621
622/// [`Cluster`] can be coordinated.
623impl coordinate::Subject for Cluster {
624    type Error = ClusterError;
625    type Options<'a> = Options<'a>;
626
627    fn start(&self, options: Self::Options<'_>) -> Result<State, Self::Error> {
628        self.start(options)
629    }
630
631    fn stop(&self) -> Result<State, Self::Error> {
632        self.stop()
633    }
634
635    fn destroy(&self) -> Result<State, Self::Error> {
636        self.destroy()
637    }
638
639    fn exists(&self) -> Result<bool, Self::Error> {
640        Ok(exists(self))
641    }
642
643    fn running(&self) -> Result<bool, Self::Error> {
644        self.running()
645    }
646}
647
648#[allow(clippy::unreadable_literal)]
649const UUID_NS: uuid::Uuid = uuid::Uuid::from_u128(93875103436633470414348750305797058811);
650
651pub type ClusterGuard = coordinate::guard::Guard<Cluster>;
652
653/// Create and start a cluster at the given path, with the given options.
654///
655/// Uses the default runtime strategy. Returns a guard which will stop the
656/// cluster when it's dropped.
657pub fn run<P: AsRef<Path>>(
658    path: P,
659    options: Options<'_>,
660) -> Result<ClusterGuard, coordinate::CoordinateError<ClusterError>> {
661    let path = path.as_ref();
662    // We have to create the data directory so that we can canonicalize its
663    // location. This is because we use the data directory's path as the basis
664    // for the lock file's name. This is duplicative – `Cluster::create` also
665    // creates the data directory – but necessary.
666    fs::create_dir_all(path)?;
667    let path = path.canonicalize()?;
668
669    let strategy = crate::runtime::strategy::Strategy::default();
670    let cluster = crate::cluster::Cluster::new(&path, strategy)?;
671
672    let lock_name = path.as_os_str().as_bytes();
673    let lock_uuid = uuid::Uuid::new_v5(&UUID_NS, lock_name);
674    let lock = crate::lock::UnlockedFile::try_from(&lock_uuid)?;
675
676    ClusterGuard::startup(lock, cluster, options)
677}