From d0b0245edbb2f6ed8285358d83b98f3334bf1b12 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Fri, 9 Apr 2021 11:54:19 +0300 Subject: feat: add "obnam init" subcommand This reads a passphrase and derives two passwords from that, and stores them next to the configuration file. The passwords aren't yet used for anything, that will come later. --- Cargo.lock | 68 +++++++++++++++++++++++++++++++++++++++ Cargo.toml | 3 ++ obnam.md | 30 ++++++++++++++++++ src/backup_run.rs | 2 ++ src/bin/obnam.rs | 71 +++++++++++++++++++++++++---------------- src/client.rs | 47 +++++++++++++++++++++++++-- src/cmd/backup.rs | 2 ++ src/cmd/init.rs | 28 ++++++++++++++++ src/cmd/mod.rs | 3 ++ src/cmd/show_config.rs | 2 +- src/error.rs | 5 +++ src/lib.rs | 1 + src/passwords.rs | 86 ++++++++++++++++++++++++++++++++++++++++++++++++++ subplot/data.py | 15 +++++++++ subplot/data.yaml | 6 ++++ 15 files changed, 337 insertions(+), 32 deletions(-) create mode 100644 src/cmd/init.rs create mode 100644 src/passwords.rs diff --git a/Cargo.lock b/Cargo.lock index 3e7d690..dc8a614 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -65,6 +65,12 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +[[package]] +name = "base64ct" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0d27fb6b6f1e43147af148af49d49329413ba781aa0d5e10979831c210173b5" + [[package]] name = "bitflags" version = "1.2.1" @@ -203,6 +209,16 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8aebca1129a03dc6dc2b127edd729435bbc4a37e1d5f4d7513165089ceb02634" +[[package]] +name = "crypto-mac" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4857fd85a0c34b3c3297875b747c1e02e06b6a0ea32dd892d8192b9ce0813ea6" +dependencies = [ + "generic-array", + "subtle", +] + [[package]] name = "derivative" version = "2.2.0" @@ -533,6 +549,16 @@ dependencies = [ "libc", ] +[[package]] +name = "hmac" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15" +dependencies = [ + "crypto-mac", + "digest", +] + [[package]] name = "http" version = "0.2.3" @@ -1008,8 +1034,11 @@ dependencies = [ "libc", "log", "log4rs", + "pbkdf2", "pretty_env_logger", + "rand 0.8.3", "reqwest", + "rpassword", "rusqlite", "serde", "serde_json", @@ -1104,6 +1133,29 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "password-hash" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85d8faea6c018131952a192ee55bd9394c51fc6f63294b668d97636e6f842d40" +dependencies = [ + "base64ct", + "rand_core 0.6.2", +] + +[[package]] +name = "pbkdf2" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf916dd32dd26297907890d99dc2740e33f6bd9073965af4ccff2967962f5508" +dependencies = [ + "base64ct", + "crypto-mac", + "hmac", + "password-hash", + "sha2", +] + [[package]] name = "percent-encoding" version = "2.1.0" @@ -1420,6 +1472,16 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "rpassword" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc936cf8a7ea60c58f030fd36a612a48f440610214dc54bc36431f9ea0c3efb" +dependencies = [ + "libc", + "winapi 0.3.9", +] + [[package]] name = "rusqlite" version = "0.24.2" @@ -1686,6 +1748,12 @@ dependencies = [ "syn", ] +[[package]] +name = "subtle" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e81da0851ada1f3e9d4312c704aa4f8806f0f9d69faaf8df2f3464b4a9437c2" + [[package]] name = "syn" version = "1.0.64" diff --git a/Cargo.toml b/Cargo.toml index 394c1bb..180d342 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,8 +18,11 @@ indicatif = "0.15" libc = "0.2" log = "0.4" log4rs = "1" +pbkdf2 = "0.7" pretty_env_logger = "0.4" +rand = "0.8" reqwest = { version = "0.11", features = ["blocking", "json"]} +rpassword = "5" rusqlite = "0.24" serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/obnam.md b/obnam.md index 959415d..7e07c50 100644 --- a/obnam.md +++ b/obnam.md @@ -1118,6 +1118,7 @@ then stdout, as JSON, matches file config.json roots: [live] server_url: https://backup.example.com verify_tls_cert: true +encrypt: false ~~~ @@ -1531,11 +1532,40 @@ Verify that trying to backup without having set a passphrase fails with an error message that clearly identifies the lack of a passphrase. +~~~scenario +given an installed obnam +and a running chunk server +and a client config based on encryption.yaml +and a file live/data.dat containing some random data +and a manifest of the directory live in live.yaml +when I try to run obnam --config encryption.yaml backup +then command fails +then stderr contains "obnam init" +~~~ + +~~~{#encryption.yaml .file .yaml .numberLines} +verify_tls_cert: false +roots: [live] +encrypt: true +~~~ + ## A passphrase can be set Set a passphrase. Verify that it's stored in a file that is only readable by it owner. Verify that a backup can be made. +~~~scenario +given an installed obnam +and a running chunk server +and a client config based on encryption.yaml +and a file live/data.dat containing some random data +and a manifest of the directory live in live.yaml +when I run obnam --config encryption.yaml init --insecure-passphrase=hunter2 +then file passwords.yaml exists +then file passwords.yaml is only readable by owner +then file passwords.yaml does not contain "hunter2" +~~~ + ## A passphrase stored insecurely is rejected Verify that a backup fails if the file where the passphrase is stored diff --git a/src/backup_run.rs b/src/backup_run.rs index c5d5382..e9d094d 100644 --- a/src/backup_run.rs +++ b/src/backup_run.rs @@ -40,6 +40,7 @@ pub type BackupResult = Result; impl<'a> InitialBackup<'a> { pub fn new(config: &ClientConfig, client: &'a BackupClient) -> BackupResult { let progress = BackupProgress::initial(); + let config = config.config(); Ok(Self { client, buffer_size: config.chunk_size, @@ -79,6 +80,7 @@ impl<'a> InitialBackup<'a> { impl<'a> IncrementalBackup<'a> { pub fn new(config: &ClientConfig, client: &'a BackupClient) -> BackupResult { + let config = config.config(); let policy = BackupPolicy::default(); Ok(Self { client, diff --git a/src/bin/obnam.rs b/src/bin/obnam.rs index 72ce7c2..2dbbaa2 100644 --- a/src/bin/obnam.rs +++ b/src/bin/obnam.rs @@ -1,10 +1,11 @@ -use anyhow::Context; use directories_next::ProjectDirs; use log::{debug, error, info, LevelFilter}; use log4rs::append::file::FileAppender; use log4rs::config::{Appender, Config, Logger, Root}; use obnam::client::ClientConfig; -use obnam::cmd::{backup, get_chunk, list, list_files, restore, show_config, show_generation}; +use obnam::cmd::{ + backup, get_chunk, init, list, list_files, restore, show_config, show_generation, +}; use std::path::{Path, PathBuf}; use structopt::StructOpt; @@ -14,21 +15,33 @@ const APPLICATION: &str = "obnam"; fn main() -> anyhow::Result<()> { let opt = Opt::from_args(); - let config = load_config(&opt)?; - setup_logging(&config.log)?; - debug!("configuration: {:#?}", config); + let config = load_config_without_passwords(&opt)?; + setup_logging(&config.config().log)?; info!("client starts"); debug!("{:?}", opt); + debug!("configuration: {:#?}", config); - let result = match opt.cmd { - Command::Backup => backup(&config), - Command::List => list(&config), - Command::ShowGeneration { gen_id } => show_generation(&config, &gen_id), - Command::ListFiles { gen_id } => list_files(&config, &gen_id), - Command::Restore { gen_id, to } => restore(&config, &gen_id, &to), - Command::GetChunk { chunk_id } => get_chunk(&config, &chunk_id), - Command::Config => show_config(&config), + let cfgname = config_filename(&opt); + let result = if let Command::Init { + insecure_passphrase, + } = opt.cmd + { + init(config.config(), &cfgname, insecure_passphrase) + } else { + let config = load_config_with_passwords(&opt)?; + match opt.cmd { + Command::Init { + insecure_passphrase: _, + } => panic!("this cannot happen"), + Command::Backup => backup(&config), + Command::List => list(&config), + Command::ShowGeneration { gen_id } => show_generation(&config, &gen_id), + Command::ListFiles { gen_id } => list_files(&config, &gen_id), + Command::Restore { gen_id, to } => restore(&config, &gen_id, &to), + Command::GetChunk { chunk_id } => get_chunk(&config, &chunk_id), + Command::Config => show_config(&config), + } }; if let Err(ref e) = result { @@ -40,21 +53,19 @@ fn main() -> anyhow::Result<()> { Ok(()) } -fn load_config(opt: &Opt) -> Result { - let config = match opt.config { - None => { - let filename = default_config(); - ClientConfig::read_config(&filename).with_context(|| { - format!( - "Couldn't read default configuration file {}", - filename.display() - ) - })? - } - Some(ref filename) => ClientConfig::read_config(&filename) - .with_context(|| format!("Couldn't read configuration file {}", filename.display()))?, - }; - Ok(config) +fn load_config_with_passwords(opt: &Opt) -> Result { + Ok(ClientConfig::read_with_passwords(&config_filename(opt))?) +} + +fn load_config_without_passwords(opt: &Opt) -> Result { + Ok(ClientConfig::read_without_passwords(&config_filename(opt))?) +} + +fn config_filename(opt: &Opt) -> PathBuf { + match opt.config { + None => default_config(), + Some(ref filename) => filename.to_path_buf(), + } } fn default_config() -> PathBuf { @@ -77,6 +88,10 @@ struct Opt { #[derive(Debug, StructOpt)] enum Command { + Init { + #[structopt(long)] + insecure_passphrase: Option, + }, Backup, List, ListFiles { diff --git a/src/client.rs b/src/client.rs index d513011..37bfe91 100644 --- a/src/client.rs +++ b/src/client.rs @@ -7,6 +7,7 @@ use crate::chunkmeta::ChunkMeta; use crate::fsentry::{FilesystemEntry, FilesystemKind}; use crate::generation::{FinishedGeneration, LocalGeneration, LocalGenerationError}; use crate::genlist::GenerationList; +use crate::passwords::{passwords_filename, PasswordError, Passwords}; use bytesize::MIB; use chrono::{DateTime, Local}; @@ -29,15 +30,48 @@ struct TentativeClientConfig { chunk_size: Option, roots: Vec, log: Option, + encrypt: Option, } #[derive(Debug, Serialize, Clone)] -pub struct ClientConfig { +pub enum ClientConfig { + Plain(ClientConfigWithoutPasswords), + WithPasswords(ClientConfigWithoutPasswords, Passwords), +} + +impl ClientConfig { + pub fn read_without_passwords(filename: &Path) -> Result { + let config = ClientConfigWithoutPasswords::read_config(filename)?; + Ok(ClientConfig::Plain(config)) + } + + pub fn read_with_passwords(filename: &Path) -> Result { + let config = ClientConfigWithoutPasswords::read_config(filename)?; + if config.encrypt { + let passwords = Passwords::load(&passwords_filename(filename)) + .map_err(ClientConfigError::PasswordsMissing)?; + Ok(ClientConfig::WithPasswords(config, passwords)) + } else { + Ok(ClientConfig::Plain(config)) + } + } + + pub fn config(&self) -> &ClientConfigWithoutPasswords { + match self { + Self::Plain(config) => &config, + Self::WithPasswords(config, _) => &config, + } + } +} + +#[derive(Debug, Serialize, Clone)] +pub struct ClientConfigWithoutPasswords { pub server_url: String, pub verify_tls_cert: bool, pub chunk_size: usize, pub roots: Vec, pub log: PathBuf, + pub encrypt: bool, } #[derive(Debug, thiserror::Error)] @@ -51,6 +85,9 @@ pub enum ClientConfigError { #[error("server URL doesn't use https: {0}")] NotHttps(String), + #[error("No passwords are set: you may need to run 'obnam init': {0}")] + PasswordsMissing(PasswordError), + #[error(transparent)] IoError(#[from] std::io::Error), @@ -60,13 +97,15 @@ pub enum ClientConfigError { pub type ClientConfigResult = Result; -impl ClientConfig { +impl ClientConfigWithoutPasswords { pub fn read_config(filename: &Path) -> ClientConfigResult { trace!("read_config: filename={:?}", filename); let config = std::fs::read_to_string(filename)?; let tentative: TentativeClientConfig = serde_yaml::from_str(&config)?; - let config = ClientConfig { + let encrypt = tentative.encrypt.or(Some(false)).unwrap(); + + let config = Self { server_url: tentative.server_url, roots: tentative.roots, verify_tls_cert: tentative.verify_tls_cert.or(Some(false)).unwrap(), @@ -75,6 +114,7 @@ impl ClientConfig { .log .or_else(|| Some(PathBuf::from(DEVNULL))) .unwrap(), + encrypt, }; config.check()?; @@ -147,6 +187,7 @@ pub struct BackupClient { impl BackupClient { pub fn new(config: &ClientConfig) -> ClientResult { info!("creating backup client with config: {:#?}", config); + let config = config.config(); let client = Client::builder() .danger_accept_invalid_certs(!config.verify_tls_cert) .build()?; diff --git a/src/cmd/backup.rs b/src/cmd/backup.rs index e6781e9..bd36a3a 100644 --- a/src/cmd/backup.rs +++ b/src/cmd/backup.rs @@ -53,6 +53,7 @@ fn initial_backup( info!("fresh backup without a previous generation"); let newtemp = NamedTempFile::new()?; let run = InitialBackup::new(config, &client)?; + let config = config.config(); let mut all_warnings = vec![]; let count = { let mut new = NascentGeneration::create(newtemp.path())?; @@ -79,6 +80,7 @@ fn incremental_backup( info!("incremental backup based on {}", old_ref); let newtemp = NamedTempFile::new()?; let mut run = IncrementalBackup::new(config, &client)?; + let config = config.config(); let mut all_warnings = vec![]; let count = { let oldtemp = NamedTempFile::new()?; diff --git a/src/cmd/init.rs b/src/cmd/init.rs new file mode 100644 index 0000000..f0ddb69 --- /dev/null +++ b/src/cmd/init.rs @@ -0,0 +1,28 @@ +use crate::client::ClientConfigWithoutPasswords; +use crate::error::ObnamError; +use crate::passwords::{passwords_filename, Passwords}; +use std::path::Path; + +const PROMPT: &str = "Obnam passphrase: "; + +pub fn init( + config: &ClientConfigWithoutPasswords, + config_filename: &Path, + insecure_passphrase: Option, +) -> Result<(), ObnamError> { + if !config.encrypt { + panic!("no encryption specified"); + } + + let passphrase = match insecure_passphrase { + Some(x) => x, + None => rpassword::read_password_from_tty(Some(PROMPT)).unwrap(), + }; + + let passwords = Passwords::new(&passphrase); + let filename = passwords_filename(config_filename); + passwords + .save(&filename) + .map_err(|err| ObnamError::PasswordSave(filename, err))?; + Ok(()) +} diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs index fc517be..70dde59 100644 --- a/src/cmd/mod.rs +++ b/src/cmd/mod.rs @@ -1,3 +1,6 @@ +mod init; +pub use init::init; + mod backup; pub use backup::backup; diff --git a/src/cmd/show_config.rs b/src/cmd/show_config.rs index b4f4cdc..8acd1c8 100644 --- a/src/cmd/show_config.rs +++ b/src/cmd/show_config.rs @@ -2,6 +2,6 @@ use crate::client::ClientConfig; use crate::error::ObnamError; pub fn show_config(config: &ClientConfig) -> Result<(), ObnamError> { - println!("{}", serde_json::to_string_pretty(config)?); + println!("{}", serde_json::to_string_pretty(&config.config())?); Ok(()) } diff --git a/src/error.rs b/src/error.rs index a905458..454bba6 100644 --- a/src/error.rs +++ b/src/error.rs @@ -3,6 +3,8 @@ use crate::client::{ClientConfigError, ClientError}; use crate::cmd::restore::RestoreError; use crate::generation::{LocalGenerationError, NascentError}; use crate::genlist::GenerationListError; +use crate::passwords::PasswordError; +use std::path::PathBuf; use std::time::SystemTimeError; use tempfile::PersistError; @@ -13,6 +15,9 @@ pub enum ObnamError { #[error(transparent)] GenerationListError(#[from] GenerationListError), + #[error("couldn't save passwords to {0}: {1}")] + PasswordSave(PathBuf, PasswordError), + #[error(transparent)] ClientError(#[from] ClientError), diff --git a/src/lib.rs b/src/lib.rs index a12b8a3..fb4c7fe 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,6 +16,7 @@ pub mod generation; pub mod genlist; pub mod index; pub mod indexedstore; +pub mod passwords; pub mod policy; pub mod server; pub mod store; diff --git a/src/passwords.rs b/src/passwords.rs new file mode 100644 index 0000000..b8ca3f5 --- /dev/null +++ b/src/passwords.rs @@ -0,0 +1,86 @@ +use pbkdf2::{ + password_hash::{PasswordHasher, SaltString}, + Pbkdf2, +}; +use rand::rngs::OsRng; +use serde::{Deserialize, Serialize}; +use std::io::prelude::Write; +use std::os::unix::fs::PermissionsExt; +use std::path::{Path, PathBuf}; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Passwords { + encryption: String, + mac: String, +} + +impl Passwords { + pub fn new(passphrase: &str) -> Self { + Self { + encryption: derive_password(passphrase), + mac: derive_password(passphrase), + } + } + + pub fn load(filename: &Path) -> Result { + let data = std::fs::read(filename) + .map_err(|err| PasswordError::Read(filename.to_path_buf(), err))?; + serde_yaml::from_slice(&data) + .map_err(|err| PasswordError::Parse(filename.to_path_buf(), err)) + } + + pub fn save(&self, filename: &Path) -> Result<(), PasswordError> { + eprintln!("saving passwords to {:?}", filename); + + let data = serde_yaml::to_string(&self).map_err(PasswordError::Serialize)?; + + let mut file = std::fs::File::create(filename) + .map_err(|err| PasswordError::Write(filename.to_path_buf(), err))?; + let metadata = file + .metadata() + .map_err(|err| PasswordError::Write(filename.to_path_buf(), err))?; + let mut permissions = metadata.permissions(); + + // Make readadable by owner only. We still have the open file + // handle, so we can write the content. + permissions.set_mode(0o400); + std::fs::set_permissions(filename, permissions) + .map_err(|err| PasswordError::Write(filename.to_path_buf(), err))?; + + // Write actual content. + file.write_all(data.as_bytes()) + .map_err(|err| PasswordError::Write(filename.to_path_buf(), err))?; + + Ok(()) + } +} + +pub fn passwords_filename(config_filename: &Path) -> PathBuf { + let mut filename = config_filename.to_path_buf(); + filename.set_file_name("passwords.yaml"); + filename +} + +fn derive_password(passphrase: &str) -> String { + let salt = SaltString::generate(&mut OsRng); + + Pbkdf2 + .hash_password_simple(passphrase.as_bytes(), salt.as_ref()) + .unwrap() + .to_string() +} + +#[derive(Debug, thiserror::Error)] +pub enum PasswordError { + #[error("failed to serialize passwords for saving: {0}")] + Serialize(serde_yaml::Error), + + #[error("failed to save passwords to {0}: {1}")] + Write(PathBuf, std::io::Error), + + #[error("failed to read passwords from {0}: {1}")] + Read(PathBuf, std::io::Error), + + #[error("failed to parse saved passwords from {0}: {1}")] + Parse(PathBuf, serde_yaml::Error), +} diff --git a/subplot/data.py b/subplot/data.py index 1455bf4..3833f2e 100644 --- a/subplot/data.py +++ b/subplot/data.py @@ -3,6 +3,7 @@ import logging import os import random import socket +import stat import yaml @@ -148,3 +149,17 @@ def manifests_match(ctx, expected=None, actual=None): assert_eq(actual_objs, []) logging.debug(f"manifests {expected} and {actual} match") + + +def file_is_readable_by_owner(ctx, filename=None): + assert_eq = globals()["assert_eq"] + + st = os.lstat(filename) + mode = stat.S_IMODE(st.st_mode) + logging.debug("file mode: %o", mode) + assert_eq(mode, 0o400) + + +def file_does_not_contain(ctx, filename=None, pattern=None): + data = open(filename).read() + assert pattern not in data diff --git a/subplot/data.yaml b/subplot/data.yaml index 64348d6..699c5b1 100644 --- a/subplot/data.yaml +++ b/subplot/data.yaml @@ -39,3 +39,9 @@ - then: "manifests {expected} and {actual} match" function: manifests_match + +- then: "file {filename} is only readable by owner" + function: file_is_readable_by_owner + +- then: "file {filename} does not contain \"{pattern:text}\"" + function: file_does_not_contain -- cgit v1.2.1 From a742941804dd6f4b1ea3f39dfe3c7072b4b50e6b Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Fri, 9 Apr 2021 17:47:58 +0300 Subject: refactor: make tests install a config in the default location This way, every step doesn't need to specify a --config option to the Obnam client. --- obnam.md | 84 ++++++++++++++++++++++++++--------------------------- subplot/client.py | 33 +++++++-------------- subplot/client.yaml | 4 +-- 3 files changed, 55 insertions(+), 66 deletions(-) diff --git a/obnam.md b/obnam.md index 7e07c50..e5a5447 100644 --- a/obnam.md +++ b/obnam.md @@ -1153,7 +1153,7 @@ given an installed obnam and a running chunk server and a client config based on ca-required.yaml and a file live/data.dat containing some random data -when I try to run obnam --config ca-required.yaml backup +when I try to run obnam backup then command fails then stderr contains "self signed certificate" ~~~ @@ -1182,11 +1182,11 @@ and a running chunk server and a client config based on smoke.yaml and a file live/data.dat containing some random data and a manifest of the directory live in live.yaml -when I run obnam --config smoke.yaml backup +when I run obnam backup then backup generation is GEN -when I run obnam --config smoke.yaml list +when I run obnam list then generation list contains -when I invoke obnam --config smoke.yaml restore rest +when I invoke obnam restore rest given a manifest of the directory live restored in rest in rest.yaml then manifests live.yaml and rest.yaml match ~~~ @@ -1221,9 +1221,9 @@ and a running chunk server and a client config based on metadata.yaml and a file live/data.dat containing some random data and a manifest of the directory live in live.yaml -when I run obnam --config metadata.yaml backup +when I run obnam backup then backup generation is GEN -when I invoke obnam --config metadata.yaml restore rest +when I invoke obnam restore rest given a manifest of the directory live restored in rest in rest.yaml then manifests live.yaml and rest.yaml match ~~~ @@ -1240,9 +1240,9 @@ and a client config based on metadata.yaml and a file live/data.dat containing some random data and file live/data.dat has mode 464 and a manifest of the directory live in live.yaml -when I run obnam --config metadata.yaml backup +when I run obnam backup then backup generation is GEN -when I invoke obnam --config metadata.yaml restore rest +when I invoke obnam restore rest given a manifest of the directory live restored in rest in rest.yaml then manifests live.yaml and rest.yaml match ~~~ @@ -1259,9 +1259,9 @@ and a file live/data.dat containing some random data and symbolink link live/link that points at data.dat and symbolink link live/broken that points at does-not-exist and a manifest of the directory live in live.yaml -when I run obnam --config metadata.yaml backup +when I run obnam backup then backup generation is GEN -when I invoke obnam --config metadata.yaml restore rest +when I invoke obnam restore rest given a manifest of the directory live restored in rest in rest.yaml then manifests live.yaml and rest.yaml match ~~~ @@ -1277,7 +1277,7 @@ given an installed obnam given a running chunk server given a client config based on tiny-chunk-size.yaml given a file live/data.dat containing "abc" -when I run obnam --config tiny-chunk-size.yaml backup +when I run obnam backup then server has 3 file chunks ~~~ @@ -1305,8 +1305,8 @@ and a running chunk server and a client config based on smoke.yaml and a file live/data.dat containing some random data and a manifest of the directory live in live.yaml -when I run obnam --config smoke.yaml backup -when I run obnam --config smoke.yaml list-files +when I run obnam backup +when I run obnam list-files then file live/data.dat was backed up because it was new ~~~ @@ -1321,9 +1321,9 @@ and a running chunk server and a client config based on smoke.yaml and a file live/data.dat containing some random data and a manifest of the directory live in live.yaml -when I run obnam --config smoke.yaml backup -when I run obnam --config smoke.yaml backup -when I run obnam --config smoke.yaml list-files +when I run obnam backup +when I run obnam backup +when I run obnam list-files then file live/data.dat was not backed up because it was unchanged ~~~ @@ -1338,10 +1338,10 @@ and a running chunk server and a client config based on smoke.yaml and a file live/data.dat containing some random data and a manifest of the directory live in live.yaml -when I run obnam --config smoke.yaml backup +when I run obnam backup given a file live/data.dat containing some random data -when I run obnam --config smoke.yaml backup -when I run obnam --config smoke.yaml list-files +when I run obnam backup +when I run obnam list-files then file live/data.dat was backed up because it was changed ~~~ @@ -1356,12 +1356,12 @@ given an installed obnam and a running chunk server and a client config based on smoke.yaml and a file live/data.dat containing some random data -when I run obnam --config smoke.yaml backup +when I run obnam backup then backup generation is GEN -when I invoke obnam --config smoke.yaml get-chunk +when I invoke obnam get-chunk then exit code is 0 when chunk on chunk server is replaced by an empty file -when I invoke obnam --config smoke.yaml get-chunk +when I invoke obnam get-chunk then command fails ~~~ @@ -1381,8 +1381,8 @@ and a file live/data.dat containing some random data and a Unix socket live/socket and a named pipe live/pipe and a manifest of the directory live in live.yaml -when I run obnam --config smoke.yaml backup -when I run obnam --config smoke.yaml restore latest rest +when I run obnam backup +when I run obnam restore latest rest given a manifest of the directory live restored in rest in rest.yaml then manifests live.yaml and rest.yaml match ~~~ @@ -1400,9 +1400,9 @@ and a running chunk server and a client config based on metadata.yaml and a file in live with a non-UTF8 filename and a manifest of the directory live in live.yaml -when I run obnam --config metadata.yaml backup +when I run obnam backup then backup generation is GEN -when I invoke obnam --config metadata.yaml restore rest +when I invoke obnam restore rest given a manifest of the directory live restored in rest in rest.yaml then manifests live.yaml and rest.yaml match ~~~ @@ -1422,9 +1422,9 @@ and a client config based on smoke.yaml and a file live/data.dat containing some random data and a file live/bad.dat containing some random data and file live/bad.dat has mode 000 -when I run obnam --config smoke.yaml backup +when I run obnam backup then backup generation is GEN -when I invoke obnam --config smoke.yaml restore rest +when I invoke obnam restore rest then file live/data.dat is restored to rest then file live/bad.dat is not restored to rest ~~~ @@ -1440,10 +1440,10 @@ and a running chunk server and a client config based on smoke.yaml and a file live/unreadable/data.dat containing some random data and file live/unreadable has mode 000 -when I run obnam --config smoke.yaml backup +when I run obnam backup then stdout contains "live/unreadable" then backup generation is GEN -when I invoke obnam --config smoke.yaml restore rest +when I invoke obnam restore rest then file live/unreadable is restored to rest then file live/unreadable/data.dat is not restored to rest ~~~ @@ -1459,10 +1459,10 @@ and a running chunk server and a client config based on smoke.yaml and a file live/dir/data.dat containing some random data and file live/dir has mode 600 -when I run obnam --config smoke.yaml backup +when I run obnam backup then stdout contains "live/dir" then backup generation is GEN -when I invoke obnam --config smoke.yaml restore rest +when I invoke obnam restore rest then file live/dir is restored to rest then file live/dir/data.dat is not restored to rest ~~~ @@ -1479,13 +1479,13 @@ and a running chunk server and a client config based on metadata.yaml given a file live/data.dat containing some random data -when I run obnam --config metadata.yaml backup +when I run obnam backup given a file live/more.dat containing some random data and a manifest of the directory live in second.yaml -when I run obnam --config metadata.yaml backup +when I run obnam backup -when I run obnam --config metadata.yaml restore latest rest +when I run obnam restore latest rest given a manifest of the directory live restored in rest in rest.yaml then manifests second.yaml and rest.yaml match ~~~ @@ -1504,9 +1504,9 @@ and a file live/one/data.dat containing some random data and a file live/two/data.dat containing some random data and a manifest of the directory live/one in one.yaml and a manifest of the directory live/two in two.yaml -when I run obnam --config roots.yaml backup +when I run obnam backup then backup generation is GEN -when I invoke obnam --config roots.yaml restore rest +when I invoke obnam restore rest given a manifest of the directory live/one restored in rest in rest-one.yaml given a manifest of the directory live/two restored in rest in rest-two.yaml then manifests one.yaml and rest-one.yaml match @@ -1538,7 +1538,7 @@ and a running chunk server and a client config based on encryption.yaml and a file live/data.dat containing some random data and a manifest of the directory live in live.yaml -when I try to run obnam --config encryption.yaml backup +when I try to run obnam backup then command fails then stderr contains "obnam init" ~~~ @@ -1560,10 +1560,10 @@ and a running chunk server and a client config based on encryption.yaml and a file live/data.dat containing some random data and a manifest of the directory live in live.yaml -when I run obnam --config encryption.yaml init --insecure-passphrase=hunter2 -then file passwords.yaml exists -then file passwords.yaml is only readable by owner -then file passwords.yaml does not contain "hunter2" +when I run obnam init --insecure-passphrase=hunter2 +then file .config/obnam/passwords.yaml exists +then file .config/obnam/passwords.yaml is only readable by owner +then file .config/obnam/passwords.yaml does not contain "hunter2" ~~~ ## A passphrase stored insecurely is rejected diff --git a/subplot/client.py b/subplot/client.py index 90642f3..1ddc772 100644 --- a/subplot/client.py +++ b/subplot/client.py @@ -26,38 +26,27 @@ def configure_client(ctx, filename=None): config["server_url"] = ctx["server_url"] logging.debug(f"client config {filename}: {config}") + dirname = os.path.expanduser("~/.config/obnam") + if not os.path.exists(dirname): + os.makedirs(dirname) + filename = os.path.join(dirname, "obnam.yaml") + logging.debug(f"configure_client: filename={filename}") with open(filename, "w") as f: yaml.safe_dump(config, stream=f) -def run_obnam_restore(ctx, filename=None, genid=None, todir=None): - genid = ctx["vars"][genid] - run_obnam_restore_with_genref(ctx, filename=filename, genref=genid, todir=todir) +def run_obnam_restore(ctx, genid=None, todir=None): + runcmd_run = globals()["runcmd_run"] + genref = ctx["vars"][genid] + runcmd_run(ctx, ["env", "RUST_LOG=obnam", "obnam", "restore", genref, todir]) -def run_obnam_restore_with_genref(ctx, filename=None, genref=None, todir=None): - runcmd_run = globals()["runcmd_run"] - runcmd_run( - ctx, - [ - "env", - "RUST_LOG=obnam", - "obnam", - "--config", - filename, - "restore", - genref, - todir, - ], - ) - - -def run_obnam_get_chunk(ctx, filename=None, gen_id=None, todir=None): +def run_obnam_get_chunk(ctx, gen_id=None, todir=None): runcmd_run = globals()["runcmd_run"] gen_id = ctx["vars"][gen_id] logging.debug(f"run_obnam_get_chunk: gen_id={gen_id}") - runcmd_run(ctx, ["obnam", "--config", filename, "get-chunk", gen_id]) + runcmd_run(ctx, ["obnam", "get-chunk", gen_id]) def capture_generation_id(ctx, varname=None): diff --git a/subplot/client.yaml b/subplot/client.yaml index eba9212..8c76e9f 100644 --- a/subplot/client.yaml +++ b/subplot/client.yaml @@ -5,10 +5,10 @@ - given: "a client config based on {filename}" function: configure_client -- when: "I invoke obnam --config {filename} restore <{genid}> {todir}" +- when: "I invoke obnam restore <{genid}> {todir}" function: run_obnam_restore -- when: "I invoke obnam --config {filename} get-chunk <{gen_id}>" +- when: "I invoke obnam get-chunk <{gen_id}>" function: run_obnam_get_chunk - then: "backup generation is {varname}" -- cgit v1.2.1