diff --git a/pbs-api-types/src/lib.rs b/pbs-api-types/src/lib.rs index aa0dd9a1..0aa9374c 100644 --- a/pbs-api-types/src/lib.rs +++ b/pbs-api-types/src/lib.rs @@ -58,6 +58,9 @@ pub use crypto::{CryptMode, Fingerprint}; pub mod file_restore; +mod remote; +pub use remote::*; + #[rustfmt::skip] #[macro_use] mod local_macros { @@ -132,6 +135,16 @@ pub const CIDR_V4_FORMAT: ApiStringFormat = ApiStringFormat::Pattern(&CIDR_V4_RE pub const CIDR_V6_FORMAT: ApiStringFormat = ApiStringFormat::Pattern(&CIDR_V6_REGEX); pub const CIDR_FORMAT: ApiStringFormat = ApiStringFormat::Pattern(&CIDR_REGEX); +pub const DNS_NAME_FORMAT: ApiStringFormat = + ApiStringFormat::Pattern(&DNS_NAME_REGEX); + +pub const DNS_NAME_OR_IP_FORMAT: ApiStringFormat = + ApiStringFormat::Pattern(&DNS_NAME_OR_IP_REGEX); + +pub const DNS_NAME_OR_IP_SCHEMA: Schema = StringSchema::new("DNS name or IP address.") + .format(&DNS_NAME_OR_IP_FORMAT) + .schema(); + pub const BACKUP_ID_SCHEMA: Schema = StringSchema::new("Backup ID.") .format(&BACKUP_ID_FORMAT) .schema(); diff --git a/pbs-config/src/lib.rs b/pbs-config/src/lib.rs index 03aa6525..9d8c730d 100644 --- a/pbs-config/src/lib.rs +++ b/pbs-config/src/lib.rs @@ -1,4 +1,5 @@ pub mod domains; +pub mod remote; use anyhow::{format_err, Error}; diff --git a/pbs-config/src/remote.rs b/pbs-config/src/remote.rs new file mode 100644 index 00000000..77d574f4 --- /dev/null +++ b/pbs-config/src/remote.rs @@ -0,0 +1,64 @@ +use anyhow::{Error}; +use lazy_static::lazy_static; +use std::collections::HashMap; + +use proxmox::api::{ + schema::*, + section_config::{ + SectionConfig, + SectionConfigData, + SectionConfigPlugin, + } +}; + +use pbs_api_types::{Remote, REMOTE_ID_SCHEMA}; + +use crate::{open_backup_lockfile, BackupLockGuard}; + +lazy_static! { + pub static ref CONFIG: SectionConfig = init(); +} + +fn init() -> SectionConfig { + let obj_schema = match Remote::API_SCHEMA { + Schema::AllOf(ref allof_schema) => allof_schema, + _ => unreachable!(), + }; + + let plugin = SectionConfigPlugin::new("remote".to_string(), Some("name".to_string()), obj_schema); + let mut config = SectionConfig::new(&REMOTE_ID_SCHEMA); + config.register_plugin(plugin); + + config +} + +pub const REMOTE_CFG_FILENAME: &str = "/etc/proxmox-backup/remote.cfg"; +pub const REMOTE_CFG_LOCKFILE: &str = "/etc/proxmox-backup/.remote.lck"; + +/// Get exclusive lock +pub fn lock_config() -> Result { + open_backup_lockfile(REMOTE_CFG_LOCKFILE, None, true) +} + +pub fn config() -> Result<(SectionConfigData, [u8;32]), Error> { + + let content = proxmox::tools::fs::file_read_optional_string(REMOTE_CFG_FILENAME)? + .unwrap_or_else(|| "".to_string()); + + let digest = openssl::sha::sha256(content.as_bytes()); + let data = CONFIG.parse(REMOTE_CFG_FILENAME, &content)?; + Ok((data, digest)) +} + +pub fn save_config(config: &SectionConfigData) -> Result<(), Error> { + let raw = CONFIG.write(REMOTE_CFG_FILENAME, &config)?; + crate::replace_backup_config(REMOTE_CFG_FILENAME, raw.as_bytes()) +} + +// shell completion helper +pub fn complete_remote_name(_arg: &str, _param: &HashMap) -> Vec { + match config() { + Ok((data, _digest)) => data.sections.iter().map(|(id, _)| id.to_string()).collect(), + Err(_) => return vec![], + } +} diff --git a/src/api2/config/remote.rs b/src/api2/config/remote.rs index f474e5f2..caf0404a 100644 --- a/src/api2/config/remote.rs +++ b/src/api2/config/remote.rs @@ -6,12 +6,13 @@ use proxmox::api::{api, ApiMethod, Router, RpcEnvironment, Permission}; use proxmox::http_err; use pbs_client::{HttpClient, HttpClientOptions}; +use pbs_api_types::{ + REMOTE_ID_SCHEMA, REMOTE_PASSWORD_SCHEMA, Remote, RemoteConfig, RemoteConfigUpdater, + Authid, PROXMOX_CONFIG_DIGEST_SCHEMA, DataStoreListItem, +}; -use crate::api2::types::*; use crate::config::cached_user_info::CachedUserInfo; -use crate::config::remote; use crate::config::acl::{PRIV_REMOTE_AUDIT, PRIV_REMOTE_MODIFY}; -use pbs_config::open_backup_lockfile; #[api( input: { @@ -20,7 +21,7 @@ use pbs_config::open_backup_lockfile; returns: { description: "The list of configured remotes (with config digest).", type: Array, - items: { type: remote::Remote }, + items: { type: Remote }, }, access: { description: "List configured remotes filtered by Remote.Audit privileges", @@ -32,13 +33,13 @@ pub fn list_remotes( _param: Value, _info: &ApiMethod, mut rpcenv: &mut dyn RpcEnvironment, -) -> Result, Error> { +) -> Result, Error> { let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?; let user_info = CachedUserInfo::new()?; - let (config, digest) = remote::config()?; + let (config, digest) = pbs_config::remote::config()?; - let mut list: Vec = config.convert_to_typed_array("remote")?; + let mut list: Vec = config.convert_to_typed_array("remote")?; // don't return password in api for remote in &mut list { remote.password = "".to_string(); @@ -64,12 +65,12 @@ pub fn list_remotes( schema: REMOTE_ID_SCHEMA, }, config: { - type: remote::RemoteConfig, + type: RemoteConfig, flatten: true, }, password: { // We expect the plain password here (not base64 encoded) - schema: remote::REMOTE_PASSWORD_SCHEMA, + schema: REMOTE_PASSWORD_SCHEMA, }, }, }, @@ -80,23 +81,23 @@ pub fn list_remotes( /// Create new remote. pub fn create_remote( name: String, - config: remote::RemoteConfig, + config: RemoteConfig, password: String, ) -> Result<(), Error> { - let _lock = open_backup_lockfile(remote::REMOTE_CFG_LOCKFILE, None, true)?; + let _lock = pbs_config::remote::config()?; - let (mut section_config, _digest) = remote::config()?; + let (mut section_config, _digest) = pbs_config::remote::config()?; if section_config.sections.get(&name).is_some() { bail!("remote '{}' already exists.", name); } - let remote = remote::Remote { name: name.clone(), config, password }; + let remote = Remote { name: name.clone(), config, password }; section_config.set_data(&name, "remote", &remote)?; - remote::save_config(§ion_config)?; + pbs_config::remote::save_config(§ion_config)?; Ok(()) } @@ -109,7 +110,7 @@ pub fn create_remote( }, }, }, - returns: { type: remote::Remote }, + returns: { type: Remote }, access: { permission: &Permission::Privilege(&["remote", "{name}"], PRIV_REMOTE_AUDIT, false), } @@ -119,9 +120,9 @@ pub fn read_remote( name: String, _info: &ApiMethod, mut rpcenv: &mut dyn RpcEnvironment, -) -> Result { - let (config, digest) = remote::config()?; - let mut data: remote::Remote = config.lookup("remote", &name)?; +) -> Result { + let (config, digest) = pbs_config::remote::config()?; + let mut data: Remote = config.lookup("remote", &name)?; data.password = "".to_string(); // do not return password in api rpcenv["digest"] = proxmox::tools::digest_to_hex(&digest).into(); Ok(data) @@ -148,13 +149,13 @@ pub enum DeletableProperty { schema: REMOTE_ID_SCHEMA, }, update: { - type: remote::RemoteConfigUpdater, + type: RemoteConfigUpdater, flatten: true, }, password: { // We expect the plain password here (not base64 encoded) optional: true, - schema: remote::REMOTE_PASSWORD_SCHEMA, + schema: REMOTE_PASSWORD_SCHEMA, }, delete: { description: "List of properties to delete.", @@ -177,22 +178,22 @@ pub enum DeletableProperty { /// Update remote configuration. pub fn update_remote( name: String, - update: remote::RemoteConfigUpdater, + update: RemoteConfigUpdater, password: Option, delete: Option>, digest: Option, ) -> Result<(), Error> { - let _lock = open_backup_lockfile(remote::REMOTE_CFG_LOCKFILE, None, true)?; + let _lock = pbs_config::remote::config()?; - let (mut config, expected_digest) = remote::config()?; + let (mut config, expected_digest) = pbs_config::remote::config()?; if let Some(ref digest) = digest { let digest = proxmox::tools::hex_to_digest(digest)?; crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?; } - let mut data: remote::Remote = config.lookup("remote", &name)?; + let mut data: Remote = config.lookup("remote", &name)?; if let Some(delete) = delete { for delete_prop in delete { @@ -221,7 +222,7 @@ pub fn update_remote( config.set_data(&name, "remote", &data)?; - remote::save_config(&config)?; + pbs_config::remote::save_config(&config)?; Ok(()) } @@ -257,9 +258,9 @@ pub fn delete_remote(name: String, digest: Option) -> Result<(), Error> } } - let _lock = open_backup_lockfile(remote::REMOTE_CFG_LOCKFILE, None, true)?; + let _lock = pbs_config::remote::config()?; - let (mut config, expected_digest) = remote::config()?; + let (mut config, expected_digest) = pbs_config::remote::config()?; if let Some(ref digest) = digest { let digest = proxmox::tools::hex_to_digest(digest)?; @@ -271,13 +272,13 @@ pub fn delete_remote(name: String, digest: Option) -> Result<(), Error> None => bail!("remote '{}' does not exist.", name), } - remote::save_config(&config)?; + pbs_config::remote::save_config(&config)?; Ok(()) } /// Helper to get client for remote.cfg entry -pub async fn remote_client(remote: remote::Remote) -> Result { +pub async fn remote_client(remote: Remote) -> Result { let options = HttpClientOptions::new_non_interactive(remote.password.clone(), remote.config.fingerprint.clone()); let client = HttpClient::new( @@ -312,8 +313,8 @@ pub async fn remote_client(remote: remote::Remote) -> Result )] /// List datastores of a remote.cfg entry pub async fn scan_remote_datastores(name: String) -> Result, Error> { - let (remote_config, _digest) = remote::config()?; - let remote: remote::Remote = remote_config.lookup("remote", &name)?; + let (remote_config, _digest) = pbs_config::remote::config()?; + let remote: Remote = remote_config.lookup("remote", &name)?; let map_remote_err = |api_err| { http_err!(INTERNAL_SERVER_ERROR, diff --git a/src/api2/pull.rs b/src/api2/pull.rs index cf4e2ef9..e38f2670 100644 --- a/src/api2/pull.rs +++ b/src/api2/pull.rs @@ -8,20 +8,20 @@ use proxmox::api::api; use proxmox::api::{ApiMethod, Router, RpcEnvironment, Permission}; use pbs_client::{HttpClient, BackupRepository}; +use pbs_api_types::{ + Remote, DATASTORE_SCHEMA, REMOTE_ID_SCHEMA, Authid, +}; use crate::server::{WorkerTask, jobstate::Job, pull::pull_store}; use crate::backup::DataStore; -use crate::api2::types::{ - DATASTORE_SCHEMA, REMOTE_ID_SCHEMA, REMOVE_VANISHED_BACKUPS_SCHEMA, Authid, -}; +use crate::api2::types::REMOVE_VANISHED_BACKUPS_SCHEMA; + use crate::config::{ - remote, sync::SyncJobConfig, acl::{PRIV_DATASTORE_BACKUP, PRIV_DATASTORE_PRUNE, PRIV_REMOTE_READ}, cached_user_info::CachedUserInfo, }; - pub fn check_pull_privs( auth_id: &Authid, store: &str, @@ -50,8 +50,8 @@ pub async fn get_pull_parameters( let tgt_store = DataStore::lookup_datastore(store)?; - let (remote_config, _digest) = remote::config()?; - let remote: remote::Remote = remote_config.lookup("remote", remote)?; + let (remote_config, _digest) = pbs_config::remote::config()?; + let remote: Remote = remote_config.lookup("remote", remote)?; let src_repo = BackupRepository::new( Some(remote.config.auth_id.clone()), diff --git a/src/api2/types/mod.rs b/src/api2/types/mod.rs index 8632eac4..697165ad 100644 --- a/src/api2/types/mod.rs +++ b/src/api2/types/mod.rs @@ -52,15 +52,9 @@ pub const SYSTEMD_DATETIME_FORMAT: ApiStringFormat = pub const HOSTNAME_FORMAT: ApiStringFormat = ApiStringFormat::Pattern(&HOSTNAME_REGEX); -pub const DNS_NAME_FORMAT: ApiStringFormat = - ApiStringFormat::Pattern(&DNS_NAME_REGEX); - pub const DNS_ALIAS_FORMAT: ApiStringFormat = ApiStringFormat::Pattern(&DNS_ALIAS_REGEX); -pub const DNS_NAME_OR_IP_FORMAT: ApiStringFormat = - ApiStringFormat::Pattern(&DNS_NAME_OR_IP_REGEX); - pub const ACL_PATH_FORMAT: ApiStringFormat = ApiStringFormat::Pattern(&ACL_PATH_REGEX); @@ -284,12 +278,6 @@ pub const VERIFICATION_SCHEDULE_SCHEMA: Schema = StringSchema::new( .type_text("") .schema(); -pub const REMOTE_ID_SCHEMA: Schema = StringSchema::new("Remote ID.") - .format(&PROXMOX_SAFE_ID_FORMAT) - .min_length(3) - .max_length(32) - .schema(); - pub const JOB_ID_SCHEMA: Schema = StringSchema::new("Job ID.") .format(&PROXMOX_SAFE_ID_FORMAT) .min_length(3) @@ -315,10 +303,6 @@ pub const HOSTNAME_SCHEMA: Schema = StringSchema::new("Hostname (as defined in R .format(&HOSTNAME_FORMAT) .schema(); -pub const DNS_NAME_OR_IP_SCHEMA: Schema = StringSchema::new("DNS name or IP address.") - .format(&DNS_NAME_OR_IP_FORMAT) - .schema(); - pub const SUBSCRIPTION_KEY_SCHEMA: Schema = StringSchema::new("Proxmox Backup Server subscription key.") .format(&SUBSCRIPTION_KEY_FORMAT) .min_length(15) diff --git a/src/bin/docgen.rs b/src/bin/docgen.rs index 1fba9baf..e0c57321 100644 --- a/src/bin/docgen.rs +++ b/src/bin/docgen.rs @@ -58,7 +58,7 @@ fn main() -> Result<(), Error> { "tape.cfg" => dump_section_config(&config::drive::CONFIG), "tape-job.cfg" => dump_section_config(&config::tape_job::CONFIG), "user.cfg" => dump_section_config(&config::user::CONFIG), - "remote.cfg" => dump_section_config(&config::remote::CONFIG), + "remote.cfg" => dump_section_config(&pbs_config::remote::CONFIG), "sync.cfg" => dump_section_config(&config::sync::CONFIG), "verification.cfg" => dump_section_config(&config::verify::CONFIG), "media-pool.cfg" => dump_section_config(&config::media_pool::CONFIG), diff --git a/src/bin/proxmox-backup-manager.rs b/src/bin/proxmox-backup-manager.rs index 93d6de57..b27a5975 100644 --- a/src/bin/proxmox-backup-manager.rs +++ b/src/bin/proxmox-backup-manager.rs @@ -380,7 +380,7 @@ fn main() { CliCommand::new(&API_METHOD_PULL_DATASTORE) .arg_param(&["remote", "remote-store", "local-store"]) .completion_cb("local-store", config::datastore::complete_datastore_name) - .completion_cb("remote", config::remote::complete_remote_name) + .completion_cb("remote", pbs_config::remote::complete_remote_name) .completion_cb("remote-store", complete_remote_datastore_name) ) .insert( diff --git a/src/bin/proxmox_backup_manager/remote.rs b/src/bin/proxmox_backup_manager/remote.rs index 8f575d80..4f55ceb5 100644 --- a/src/bin/proxmox_backup_manager/remote.rs +++ b/src/bin/proxmox_backup_manager/remote.rs @@ -3,8 +3,10 @@ use serde_json::Value; use proxmox::api::{api, cli::*, RpcEnvironment, ApiHandler}; -use proxmox_backup::config; -use proxmox_backup::api2::{self, types::* }; +use pbs_api_types::REMOTE_ID_SCHEMA; + +use proxmox_backup::api2; + #[api( input: { @@ -77,7 +79,7 @@ pub fn remote_commands() -> CommandLineInterface { "show", CliCommand::new(&API_METHOD_SHOW_REMOTE) .arg_param(&["name"]) - .completion_cb("name", config::remote::complete_remote_name) + .completion_cb("name", pbs_config::remote::complete_remote_name) ) .insert( "create", @@ -89,13 +91,13 @@ pub fn remote_commands() -> CommandLineInterface { "update", CliCommand::new(&api2::config::remote::API_METHOD_UPDATE_REMOTE) .arg_param(&["name"]) - .completion_cb("name", config::remote::complete_remote_name) + .completion_cb("name", pbs_config::remote::complete_remote_name) ) .insert( "remove", CliCommand::new(&api2::config::remote::API_METHOD_DELETE_REMOTE) .arg_param(&["name"]) - .completion_cb("name", config::remote::complete_remote_name) + .completion_cb("name", pbs_config::remote::complete_remote_name) ); cmd_def.into() diff --git a/src/bin/proxmox_backup_manager/sync.rs b/src/bin/proxmox_backup_manager/sync.rs index f05f0c8d..c93ef294 100644 --- a/src/bin/proxmox_backup_manager/sync.rs +++ b/src/bin/proxmox_backup_manager/sync.rs @@ -85,7 +85,7 @@ pub fn sync_job_commands() -> CommandLineInterface { .completion_cb("id", config::sync::complete_sync_job_id) .completion_cb("schedule", config::datastore::complete_calendar_event) .completion_cb("store", config::datastore::complete_datastore_name) - .completion_cb("remote", config::remote::complete_remote_name) + .completion_cb("remote", pbs_config::remote::complete_remote_name) .completion_cb("remote-store", crate::complete_remote_datastore_name) ) .insert("update", diff --git a/src/config/mod.rs b/src/config/mod.rs index 05e0dcb7..eb694aa3 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -20,7 +20,6 @@ pub mod cached_user_info; pub mod datastore; pub mod network; pub mod node; -pub mod remote; pub mod sync; pub mod tfa; pub mod token_shadow; diff --git a/src/config/remote.rs b/src/config/remote.rs deleted file mode 100644 index a50b319f..00000000 --- a/src/config/remote.rs +++ /dev/null @@ -1,134 +0,0 @@ -use anyhow::{Error}; -use lazy_static::lazy_static; -use std::collections::HashMap; -use serde::{Serialize, Deserialize}; - -use proxmox::api::{ - api, - schema::*, - section_config::{ - SectionConfig, - SectionConfigData, - SectionConfigPlugin, - } -}; - -use crate::api2::types::*; - -lazy_static! { - pub static ref CONFIG: SectionConfig = init(); -} - -pub const REMOTE_PASSWORD_SCHEMA: Schema = StringSchema::new("Password or auth token for remote host.") - .format(&PASSWORD_FORMAT) - .min_length(1) - .max_length(1024) - .schema(); - -pub const REMOTE_PASSWORD_BASE64_SCHEMA: Schema = StringSchema::new("Password or auth token for remote host (stored as base64 string).") - .format(&PASSWORD_FORMAT) - .min_length(1) - .max_length(1024) - .schema(); - -#[api( - properties: { - comment: { - optional: true, - schema: SINGLE_LINE_COMMENT_SCHEMA, - }, - host: { - schema: DNS_NAME_OR_IP_SCHEMA, - }, - port: { - optional: true, - description: "The (optional) port", - type: u16, - }, - "auth-id": { - type: Authid, - }, - fingerprint: { - optional: true, - schema: CERT_FINGERPRINT_SHA256_SCHEMA, - }, - }, -)] -#[derive(Serialize,Deserialize,Updater)] -#[serde(rename_all = "kebab-case")] -/// Remote configuration properties. -pub struct RemoteConfig { - #[serde(skip_serializing_if="Option::is_none")] - pub comment: Option, - pub host: String, - #[serde(skip_serializing_if="Option::is_none")] - pub port: Option, - pub auth_id: Authid, - #[serde(skip_serializing_if="Option::is_none")] - pub fingerprint: Option, -} - -#[api( - properties: { - name: { - schema: REMOTE_ID_SCHEMA, - }, - config: { - type: RemoteConfig, - }, - password: { - schema: REMOTE_PASSWORD_BASE64_SCHEMA, - }, - }, -)] -#[derive(Serialize,Deserialize)] -#[serde(rename_all = "kebab-case")] -/// Remote properties. -pub struct Remote { - pub name: String, - // Note: The stored password is base64 encoded - #[serde(skip_serializing_if="String::is_empty")] - #[serde(with = "proxmox::tools::serde::string_as_base64")] - pub password: String, - #[serde(flatten)] - pub config: RemoteConfig, -} - -fn init() -> SectionConfig { - let obj_schema = match Remote::API_SCHEMA { - Schema::AllOf(ref allof_schema) => allof_schema, - _ => unreachable!(), - }; - - let plugin = SectionConfigPlugin::new("remote".to_string(), Some("name".to_string()), obj_schema); - let mut config = SectionConfig::new(&REMOTE_ID_SCHEMA); - config.register_plugin(plugin); - - config -} - -pub const REMOTE_CFG_FILENAME: &str = "/etc/proxmox-backup/remote.cfg"; -pub const REMOTE_CFG_LOCKFILE: &str = "/etc/proxmox-backup/.remote.lck"; - -pub fn config() -> Result<(SectionConfigData, [u8;32]), Error> { - - let content = proxmox::tools::fs::file_read_optional_string(REMOTE_CFG_FILENAME)? - .unwrap_or_else(|| "".to_string()); - - let digest = openssl::sha::sha256(content.as_bytes()); - let data = CONFIG.parse(REMOTE_CFG_FILENAME, &content)?; - Ok((data, digest)) -} - -pub fn save_config(config: &SectionConfigData) -> Result<(), Error> { - let raw = CONFIG.write(REMOTE_CFG_FILENAME, &config)?; - pbs_config::replace_backup_config(REMOTE_CFG_FILENAME, raw.as_bytes()) -} - -// shell completion helper -pub fn complete_remote_name(_arg: &str, _param: &HashMap) -> Vec { - match config() { - Ok((data, _digest)) => data.sections.iter().map(|(id, _)| id.to_string()).collect(), - Err(_) => return vec![], - } -}