//! migrator is a tool to run migrations built with the migration-helpers library.
//!
//! It must be given:
//! * a data store to migrate
//! * a version to migrate it to
//! * where to find migration binaries
//!
//! Given those, it will:
//! * confirm that the given data store has the appropriate versioned symlink structure
//! * find the version of the given data store
//! * find migrations between the two versions
//! * if there are migrations:
//! * run the migrations; the transformed data becomes the new data store
//! * if there are *no* migrations:
//! * just symlink to the old data store
//! * do symlink flips so the new version takes the place of the original
//!
//! To understand motivation and more about the overall process, look at the migration system
//! documentation, one level up.
#[macro_use]
extern crate log;
use args::Args;
use direction::Direction;
use error::Result;
use nix::{dir::Dir, fcntl::OFlag, sys::stat::Mode, unistd::fsync};
use rand::{distributions::Alphanumeric, thread_rng, Rng};
use semver::Version;
use simplelog::{Config as LogConfig, SimpleLogger};
use snafu::{ensure, OptionExt, ResultExt};
use std::convert::TryInto;
use std::env;
use std::fs::{self, File};
use std::os::unix::fs::symlink;
use std::os::unix::io::AsRawFd;
use std::path::{Path, PathBuf};
use std::process;
use tough::{ExpirationEnforcement, FilesystemTransport, RepositoryLoader};
use update_metadata::Manifest;
use url::Url;
mod args;
mod direction;
mod error;
#[cfg(test)]
mod test;
// Returning a Result from main makes it print a Debug representation of the error, but with Snafu
// we have nice Display representations of the error, so we wrap "main" (run) and print any error.
// https://github.com/shepmaster/snafu/issues/110
fn main() {
let args = Args::from_env(env::args());
// SimpleLogger will send errors to stderr and anything less to stdout.
if let Err(e) = SimpleLogger::init(args.log_level, LogConfig::default()) {
eprintln!("{}", e);
process::exit(1);
}
if let Err(e) = run(&args) {
eprintln!("{}", e);
process::exit(1);
}
}
fn get_current_version
(datastore_dir: P) -> Result
where
P: AsRef,
{
let datastore_dir = datastore_dir.as_ref();
// Find the current patch version link, which contains our full version number
let current = datastore_dir.join("current");
let major = datastore_dir
.join(fs::read_link(¤t).context(error::LinkReadSnafu { link: current })?);
let minor =
datastore_dir.join(fs::read_link(&major).context(error::LinkReadSnafu { link: major })?);
let patch =
datastore_dir.join(fs::read_link(&minor).context(error::LinkReadSnafu { link: minor })?);
// Pull out the basename of the path, which contains the version
let version_os_str = patch
.file_name()
.context(error::DataStoreLinkToRootSnafu { path: &patch })?;
let mut version_str = version_os_str
.to_str()
.context(error::DataStorePathNotUTF8Snafu { path: &patch })?;
// Allow 'v' at the start so the links have clearer names for humans
if version_str.starts_with('v') {
version_str = &version_str[1..];
}
Version::parse(version_str).context(error::InvalidDataStoreVersionSnafu { path: &patch })
}
pub(crate) fn run(args: &Args) -> Result<()> {
// Get the directory we're working in.
let datastore_dir = args
.datastore_path
.parent()
.context(error::DataStoreLinkToRootSnafu {
path: &args.datastore_path,
})?;
let current_version = get_current_version(datastore_dir)?;
let direction = Direction::from_versions(¤t_version, &args.migrate_to_version)
.unwrap_or_else(|| {
info!(
"Requested version {} matches version of given datastore at '{}'; nothing to do",
args.migrate_to_version,
args.datastore_path.display()
);
process::exit(0);
});
// create URLs from the metadata and targets directory paths
let metadata_base_url = Url::from_directory_path(&args.metadata_directory).map_err(|_| {
error::Error::DirectoryUrl {
path: args.metadata_directory.clone(),
}
})?;
let targets_base_url =
url::Url::from_directory_path(&args.migration_directory).map_err(|_| {
error::Error::DirectoryUrl {
path: args.migration_directory.clone(),
}
})?;
// open a reader to the root.json file
let root_file = File::open(&args.root_path).with_context(|_| error::OpenRootSnafu {
path: args.root_path.clone(),
})?;
// We will load the locally cached TUF repository to obtain the manifest. The Repository is
// loaded using a `TempDir` for its internal Datastore (this is the default). Part of using a
// `TempDir` is disabling timestamp checking, because we want an instance to still come up and
// run migrations regardless of the how the system time relates to what we have cached (for
// example if someone runs an update, then shuts down the instance for several weeks, beyond the
// expiration of at least the cached timestamp.json before booting it back up again). We also
// use a `TempDir` because see no value in keeping a datastore around. The latest known
// versions of the repository metadata will always be the versions of repository metadata we
// have cached on the disk. More info at `ExpirationEnforcement::Unsafe` below.
// Failure to load the TUF repo at the expected location is a serious issue because updog should
// always create a TUF repo that contains at least the manifest, even if there are no migrations.
let repo = RepositoryLoader::new(root_file, metadata_base_url, targets_base_url)
.transport(FilesystemTransport)
// The threats TUF mitigates are more than the threats we are attempting to mitigate
// here by caching signatures for migrations locally and using them after a reboot but
// prior to Internet connectivity. We are caching the TUF repo and use it while offline
// after a reboot to mitigate binaries being added or modified in the migrations
// directory; the TUF repo is simply a code signing method we already have in place,
// even if it's not one that initially makes sense for this use case. So, we don't care
// if the targets expired between updog downloading them and now.
.expiration_enforcement(ExpirationEnforcement::Unsafe)
.load()
.context(error::RepoLoadSnafu)?;
let manifest = load_manifest(&repo)?;
let migrations =
update_metadata::find_migrations(¤t_version, &args.migrate_to_version, &manifest)
.context(error::FindMigrationsSnafu)?;
if migrations.is_empty() {
// Not all new OS versions need to change the data store format. If there's been no
// change, we can just link to the last version rather than making a copy.
// (Note: we link to the fully resolved directory, args.datastore_path, so we don't
// have a chain of symlinks that could go past the maximum depth.)
flip_to_new_version(&args.migrate_to_version, &args.datastore_path)?;
} else {
let copy_path = run_migrations(
&repo,
direction,
&migrations,
&args.datastore_path,
&args.migrate_to_version,
)?;
flip_to_new_version(&args.migrate_to_version, copy_path)?;
}
Ok(())
}
// =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^=
/// Generates a random ID, affectionately known as a 'rando', that can be used to avoid timing
/// issues and identify unique migration attempts.
fn rando() -> String {
thread_rng()
.sample_iter(&Alphanumeric)
.take(16)
.map(char::from)
.collect()
}
/// Generates a path for a new data store, given the path of the existing data store,
/// the new version number, and a random "copy id" to append.
fn new_datastore_location(from: P, new_version: &Version) -> Result
where
P: AsRef,
{
let to = from
.as_ref()
.with_file_name(format!("v{}_{}", new_version, rando()));
ensure!(
!to.exists(),
error::NewVersionAlreadyExistsSnafu {
version: new_version.clone(),
path: to
}
);
debug!(
"New data store is being built at work location {}",
to.display()
);
Ok(to)
}
/// Runs the given migrations in their given order. The given direction is passed to each
/// migration so it knows which direction we're migrating.
///
/// The given data store is used as a starting point; each migration is given the output of the
/// previous migration, and the final output becomes the new data store.
fn run_migrations(
repository: &tough::Repository,
direction: Direction,
migrations: &[S],
source_datastore: P,
new_version: &Version,
) -> Result
where
P: AsRef,
S: AsRef,
{
// We start with the given source_datastore, updating this after each migration to point to the
// output of the previous one.
let mut source_datastore = source_datastore.as_ref();
// We create a new data store (below) to serve as the target of each migration. (Start at
// source just to have the right type; we know we have migrations at this point.)
let mut target_datastore = source_datastore.to_owned();
// The most recent, "good", datastore. We keep it around for debugging purposes in case we
// encounter an error before reaching the final one. Once we reach final we delete the last
// intermediate_datastore.
let mut intermediate_datastore = Option::default();
for migration in migrations {
let migration = migration.as_ref();
let migration = migration
.try_into()
.context(error::TargetNameSnafu { target: migration })?;
// get the migration from the repo
let lz4_bytes = repository
.read_target(&migration)
.context(error::LoadMigrationSnafu {
migration: migration.raw(),
})?
.context(error::MigrationNotFoundSnafu {
migration: migration.raw(),
})?;
// Add an LZ4 decoder so the bytes will be deflated on read
let mut reader = lz4::Decoder::new(lz4_bytes).context(error::Lz4DecodeSnafu {
migration: migration.raw(),
})?;
// Create a sealed command with pentacle, so we can run the verified bytes from memory
let mut command =
pentacle::SealedCommand::new(&mut reader).context(error::SealMigrationSnafu)?;
// Point each migration in the right direction, and at the given data store.
command.arg(direction.to_string());
command.args(&[
"--source-datastore".to_string(),
source_datastore.display().to_string(),
]);
// Create a new output location for this migration.
target_datastore = new_datastore_location(source_datastore, new_version)?;
command.args(&[
"--target-datastore".to_string(),
target_datastore.display().to_string(),
]);
info!("Running migration '{}'", migration.raw());
debug!("Migration command: {:?}", command);
let output = command.output().context(error::StartMigrationSnafu)?;
if !output.stdout.is_empty() {
debug!(
"Migration stdout: {}",
String::from_utf8_lossy(&output.stdout)
);
} else {
debug!("No migration stdout");
}
if !output.stderr.is_empty() {
let stderr = String::from_utf8_lossy(&output.stderr);
// We want to see migration stderr on the console, so log at error level.
error!("Migration stderr: {}", stderr);
} else {
debug!("No migration stderr");
}
ensure!(
output.status.success(),
error::MigrationFailureSnafu { output }
);
// If an intermediate datastore exists from a previous loop, delete it.
if let Some(path) = &intermediate_datastore {
delete_intermediate_datastore(path);
}
// Remember the location of the target_datastore to delete it in the next loop iteration
// (i.e if it was an intermediate).
intermediate_datastore = Some(target_datastore.clone());
source_datastore = &target_datastore;
}
Ok(target_datastore)
}
// Try to delete an intermediate datastore if it exists. If it fails to delete, print an error.
fn delete_intermediate_datastore(path: &PathBuf) {
// Even if we fail to remove an intermediate data store, we don't want to fail the upgrade -
// just let someone know for later cleanup.
trace!("Removing intermediate data store at {}", path.display());
if let Err(e) = fs::remove_dir_all(path) {
error!(
"Failed to remove intermediate data store at '{}': {}",
path.display(),
e
);
}
}
/// Atomically flips version symlinks to point to the given "to" datastore so that it becomes live.
///
/// This includes:
/// * pointing the new patch version to the given `to_datastore`
/// * pointing the minor version to the patch version
/// * pointing the major version to the minor version
/// * pointing the 'current' link to the major version
/// * fsyncing the directory to disk
fn flip_to_new_version(version: &Version, to_datastore: P) -> Result<()>
where
P: AsRef,
{
// Get the directory we're working in.
let to_dir = to_datastore
.as_ref()
.parent()
.context(error::DataStoreLinkToRootSnafu {
path: to_datastore.as_ref(),
})?;
// We need a file descriptor for the directory so we can fsync after the symlink swap.
let raw_dir = Dir::open(
to_dir,
// Confirm it's a directory
OFlag::O_DIRECTORY,
// (mode doesn't matter for opening a directory)
Mode::empty(),
)
.context(error::DataStoreDirOpenSnafu { path: &to_dir })?;
// Get a unique temporary path in the directory; we need this to atomically swap.
let temp_link = to_dir.join(rando());
// Build the path to the 'current' link; this is what we're atomically swapping from
// pointing at the old major version to pointing at the new major version.
// Example: /path/to/datastore/current
let current_version_link = to_dir.join("current");
// Build the path to the major version link; this is what we're atomically swapping from
// pointing at the old minor version to pointing at the new minor version.
// Example: /path/to/datastore/v1
let major_version_link = to_dir.join(format!("v{}", version.major));
// Build the path to the minor version link; this is what we're atomically swapping from
// pointing at the old patch version to pointing at the new patch version.
// Example: /path/to/datastore/v1.5
let minor_version_link = to_dir.join(format!("v{}.{}", version.major, version.minor));
// Build the path to the patch version link. If this already exists, it's because we've
// previously tried to migrate to this version. We point it at the full `to_datastore`
// path.
// Example: /path/to/datastore/v1.5.2
let patch_version_link = to_dir.join(format!(
"v{}.{}.{}",
version.major, version.minor, version.patch
));
// Get the final component of the paths we're linking to, so we can use relative links instead
// of absolute, for understandability.
let to_target = to_datastore
.as_ref()
.file_name()
.context(error::DataStoreLinkToRootSnafu {
path: to_datastore.as_ref(),
})?;
let patch_target = patch_version_link
.file_name()
.context(error::DataStoreLinkToRootSnafu {
path: to_datastore.as_ref(),
})?;
let minor_target = minor_version_link
.file_name()
.context(error::DataStoreLinkToRootSnafu {
path: to_datastore.as_ref(),
})?;
let major_target = major_version_link
.file_name()
.context(error::DataStoreLinkToRootSnafu {
path: to_datastore.as_ref(),
})?;
// =^..^= =^..^= =^..^= =^..^=
debug!(
"Flipping {} to point to {}",
patch_version_link.display(),
to_target.to_string_lossy(),
);
// Create a symlink from the patch version to the new data store. We create it at a temporary
// path so we can atomically swap it into the real path with a rename call.
// This will point at, for example, /path/to/datastore/v1.5.2_0123456789abcdef
symlink(to_target, &temp_link).context(error::LinkCreateSnafu { path: &temp_link })?;
// Atomically swap the link into place, so that the patch version link points to the new data
// store copy.
fs::rename(&temp_link, &patch_version_link).context(error::LinkSwapSnafu {
link: &patch_version_link,
})?;
// =^..^= =^..^= =^..^= =^..^=
debug!(
"Flipping {} to point to {}",
minor_version_link.display(),
patch_target.to_string_lossy(),
);
// Create a symlink from the minor version to the new patch version.
// This will point at, for example, /path/to/datastore/v1.5.2
symlink(patch_target, &temp_link).context(error::LinkCreateSnafu { path: &temp_link })?;
// Atomically swap the link into place, so that the minor version link points to the new patch
// version.
fs::rename(&temp_link, &minor_version_link).context(error::LinkSwapSnafu {
link: &minor_version_link,
})?;
// =^..^= =^..^= =^..^= =^..^=
debug!(
"Flipping {} to point to {}",
major_version_link.display(),
minor_target.to_string_lossy(),
);
// Create a symlink from the major version to the new minor version.
// This will point at, for example, /path/to/datastore/v1.5
symlink(minor_target, &temp_link).context(error::LinkCreateSnafu { path: &temp_link })?;
// Atomically swap the link into place, so that the major version link points to the new minor
// version.
fs::rename(&temp_link, &major_version_link).context(error::LinkSwapSnafu {
link: &major_version_link,
})?;
// =^..^= =^..^= =^..^= =^..^=
debug!(
"Flipping {} to point to {}",
current_version_link.display(),
major_target.to_string_lossy(),
);
// Create a symlink from 'current' to the new major version.
// This will point at, for example, /path/to/datastore/v1
symlink(major_target, &temp_link).context(error::LinkCreateSnafu { path: &temp_link })?;
// Atomically swap the link into place, so that 'current' points to the new major version.
fs::rename(&temp_link, ¤t_version_link).context(error::LinkSwapSnafu {
link: ¤t_version_link,
})?;
// =^..^= =^..^= =^..^= =^..^=
// fsync the directory so the links point to the new version even if we crash right after
// this. If fsync fails, warn but continue, because we likely can't swap the links back
// without hitting the same failure.
fsync(raw_dir.as_raw_fd()).unwrap_or_else(|e| {
warn!(
"fsync of data store directory '{}' failed, update may disappear if we crash now: {}",
to_dir.display(),
e
)
});
Ok(())
}
fn load_manifest(repository: &tough::Repository) -> Result {
let target = "manifest.json";
let target = target
.try_into()
.context(error::TargetNameSnafu { target })?;
Manifest::from_json(
repository
.read_target(&target)
.context(error::ManifestLoadSnafu)?
.context(error::ManifestNotFoundSnafu)?,
)
.context(error::ManifestParseSnafu)
}