]> git.proxmox.com Git - proxmox-backup.git/blame - src/api2/pull.rs
pull/sync: treat unset max-depth as full recursion
[proxmox-backup.git] / src / api2 / pull.rs
CommitLineData
eb506c83 1//! Sync datastore from remote server
6e9e6c7a 2use std::convert::TryFrom;
eb506c83 3
07ad6470 4use anyhow::{format_err, Error};
dc7a5b34 5use futures::{future::FutureExt, select};
de8ec041 6
dc7a5b34 7use proxmox_router::{ApiMethod, Permission, Router, RpcEnvironment};
6ef1b649 8use proxmox_schema::api;
d5790a9f 9use proxmox_sys::task_log;
de8ec041 10
6afdda88 11use pbs_api_types::{
c06c1b4b
FG
12 Authid, BackupNamespace, GroupFilter, RateLimitConfig, SyncJobConfig, DATASTORE_SCHEMA,
13 GROUP_FILTER_LIST_SCHEMA, NS_MAX_DEPTH_SCHEMA, PRIV_DATASTORE_BACKUP, PRIV_DATASTORE_PRUNE,
14 PRIV_REMOTE_READ, REMOTE_ID_SCHEMA, REMOVE_VANISHED_BACKUPS_SCHEMA,
6afdda88 15};
1ec0d70d 16use pbs_config::CachedUserInfo;
dc7a5b34 17use proxmox_rest_server::WorkerTask;
2b7f8dd5 18
6e9e6c7a 19use crate::server::jobstate::Job;
dc7a5b34 20use crate::server::pull::{pull_store, PullParameters};
de8ec041 21
268687dd 22pub fn check_pull_privs(
e6dc35ac 23 auth_id: &Authid,
268687dd 24 store: &str,
c06c1b4b 25 ns: Option<&str>,
268687dd
DC
26 remote: &str,
27 remote_store: &str,
28 delete: bool,
29) -> Result<(), Error> {
268687dd
DC
30 let user_info = CachedUserInfo::new()?;
31
c06c1b4b
FG
32 let local_store_ns_acl_path = match ns {
33 Some(ns) => vec!["datastore", store, ns],
34 None => vec!["datastore", store],
35 };
36
37 user_info.check_privs(
38 auth_id,
39 &local_store_ns_acl_path,
40 PRIV_DATASTORE_BACKUP,
41 false,
42 )?;
dc7a5b34
TL
43 user_info.check_privs(
44 auth_id,
45 &["remote", remote, remote_store],
46 PRIV_REMOTE_READ,
47 false,
48 )?;
268687dd
DC
49
50 if delete {
c06c1b4b
FG
51 user_info.check_privs(
52 auth_id,
53 &local_store_ns_acl_path,
54 PRIV_DATASTORE_PRUNE,
55 false,
56 )?;
268687dd
DC
57 }
58
59 Ok(())
60}
61
6e9e6c7a
FG
62impl TryFrom<&SyncJobConfig> for PullParameters {
63 type Error = Error;
64
65 fn try_from(sync_job: &SyncJobConfig) -> Result<Self, Self::Error> {
66 PullParameters::new(
67 &sync_job.store,
c06c1b4b 68 sync_job.ns.clone().unwrap_or_default(),
6e9e6c7a
FG
69 &sync_job.remote,
70 &sync_job.remote_store,
c06c1b4b 71 sync_job.remote_ns.clone().unwrap_or_default(),
dc7a5b34
TL
72 sync_job
73 .owner
74 .as_ref()
75 .unwrap_or_else(|| Authid::root_auth_id())
76 .clone(),
6e9e6c7a 77 sync_job.remove_vanished,
c06c1b4b 78 sync_job.max_depth,
062edce2 79 sync_job.group_filter.clone(),
2d5287fb 80 sync_job.limit.clone(),
6e9e6c7a
FG
81 )
82 }
268687dd
DC
83}
84
42b68f72 85pub fn do_sync_job(
713b66b6 86 mut job: Job,
42b68f72 87 sync_job: SyncJobConfig,
e6dc35ac 88 auth_id: &Authid,
02543a5c 89 schedule: Option<String>,
bfa942c0 90 to_stdout: bool,
42b68f72 91) -> Result<String, Error> {
dc7a5b34 92 let job_id = format!(
c06c1b4b 93 "{}:{}:{}:{}:{}",
dc7a5b34
TL
94 sync_job.remote,
95 sync_job.remote_store,
96 sync_job.store,
c06c1b4b 97 sync_job.ns.clone().unwrap_or_default(),
dc7a5b34
TL
98 job.jobname()
99 );
713b66b6 100 let worker_type = job.jobtype().to_string();
42b68f72 101
f47c1d3a 102 let (email, notify) = crate::server::lookup_datastore_notify_settings(&sync_job.store);
9e733dae 103
02543a5c 104 let upid_str = WorkerTask::spawn(
713b66b6 105 &worker_type,
dbd45a72 106 Some(job_id.clone()),
049a22a3 107 auth_id.to_string(),
bfa942c0 108 to_stdout,
02543a5c 109 move |worker| async move {
02543a5c 110 job.start(&worker.upid().to_string())?;
42b68f72 111
02543a5c 112 let worker2 = worker.clone();
9e733dae 113 let sync_job2 = sync_job.clone();
42b68f72 114
02543a5c 115 let worker_future = async move {
6e9e6c7a
FG
116 let pull_params = PullParameters::try_from(&sync_job)?;
117 let client = pull_params.client().await?;
02543a5c 118
1ec0d70d 119 task_log!(worker, "Starting datastore sync job '{}'", job_id);
02543a5c 120 if let Some(event_str) = schedule {
1ec0d70d 121 task_log!(worker, "task triggered by schedule '{}'", event_str);
02543a5c 122 }
1ec0d70d
DM
123 task_log!(
124 worker,
125 "sync datastore '{}' from '{}/{}'",
126 sync_job.store,
127 sync_job.remote,
128 sync_job.remote_store,
129 );
02543a5c 130
d9aad37f 131 pull_store(&worker, &client, pull_params).await?;
02543a5c 132
1ec0d70d 133 task_log!(worker, "sync job '{}' end", &job_id);
02543a5c
DC
134
135 Ok(())
136 };
137
dc7a5b34
TL
138 let mut abort_future = worker2
139 .abort_future()
140 .map(|_| Err(format_err!("sync aborted")));
02543a5c 141
dc7a5b34 142 let result = select! {
02543a5c
DC
143 worker = worker_future.fuse() => worker,
144 abort = abort_future => abort,
145 };
146
9e733dae 147 let status = worker2.create_state(&result);
02543a5c
DC
148
149 match job.finish(status) {
dc7a5b34 150 Ok(_) => {}
02543a5c
DC
151 Err(err) => {
152 eprintln!("could not finish job state: {}", err);
153 }
154 }
155
9e733dae 156 if let Some(email) = email {
dc7a5b34
TL
157 if let Err(err) =
158 crate::server::send_sync_status(&email, notify, &sync_job2, &result)
159 {
9e733dae
DM
160 eprintln!("send sync notification failed: {}", err);
161 }
162 }
163
164 result
dc7a5b34
TL
165 },
166 )?;
42b68f72
DC
167
168 Ok(upid_str)
169}
170
de8ec041
DM
171#[api(
172 input: {
173 properties: {
174 store: {
175 schema: DATASTORE_SCHEMA,
176 },
c06c1b4b
FG
177 ns: {
178 type: BackupNamespace,
179 optional: true,
180 },
94609e23
DM
181 remote: {
182 schema: REMOTE_ID_SCHEMA,
de8ec041
DM
183 },
184 "remote-store": {
185 schema: DATASTORE_SCHEMA,
186 },
c06c1b4b
FG
187 "remote-ns": {
188 type: BackupNamespace,
189 optional: true,
190 },
b4900286
DM
191 "remove-vanished": {
192 schema: REMOVE_VANISHED_BACKUPS_SCHEMA,
4b4eba0b 193 optional: true,
4b4eba0b 194 },
c06c1b4b
FG
195 "max-depth": {
196 schema: NS_MAX_DEPTH_SCHEMA,
197 optional: true,
198 },
062edce2 199 "group-filter": {
71e53463
FG
200 schema: GROUP_FILTER_LIST_SCHEMA,
201 optional: true,
202 },
2d5287fb
DM
203 limit: {
204 type: RateLimitConfig,
205 flatten: true,
206 }
de8ec041
DM
207 },
208 },
404d78c4 209 access: {
365f0f72 210 // Note: used parameters are no uri parameters, so we need to test inside function body
54552dda 211 description: r###"The user needs Datastore.Backup privilege on '/datastore/{store}',
8247db5b 212and needs to own the backup group. Remote.Read is required on '/remote/{remote}/{remote-store}'.
ec67af9a 213The delete flag additionally requires the Datastore.Prune privilege on '/datastore/{store}'.
54552dda 214"###,
365f0f72 215 permission: &Permission::Anybody,
404d78c4 216 },
de8ec041 217)]
eb506c83 218/// Sync store from other repository
dc7a5b34 219async fn pull(
de8ec041 220 store: String,
c06c1b4b 221 ns: Option<BackupNamespace>,
94609e23 222 remote: String,
de8ec041 223 remote_store: String,
c06c1b4b 224 remote_ns: Option<BackupNamespace>,
b4900286 225 remove_vanished: Option<bool>,
c06c1b4b 226 max_depth: Option<usize>,
062edce2 227 group_filter: Option<Vec<GroupFilter>>,
2d5287fb 228 limit: RateLimitConfig,
de8ec041
DM
229 _info: &ApiMethod,
230 rpcenv: &mut dyn RpcEnvironment,
231) -> Result<String, Error> {
e6dc35ac 232 let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
61ef4ae8 233 let delete = remove_vanished.unwrap_or(false);
4b4eba0b 234
c06c1b4b 235 let ns = ns.unwrap_or_default();
c06c1b4b
FG
236 let ns_str = if ns.is_root() {
237 None
238 } else {
239 Some(ns.to_string())
240 };
241
242 check_pull_privs(
243 &auth_id,
244 &store,
245 ns_str.as_deref(),
246 &remote,
247 &remote_store,
248 delete,
249 )?;
de8ec041 250
6e9e6c7a
FG
251 let pull_params = PullParameters::new(
252 &store,
c06c1b4b 253 ns,
6e9e6c7a
FG
254 &remote,
255 &remote_store,
c06c1b4b 256 remote_ns.unwrap_or_default(),
6e9e6c7a
FG
257 auth_id.clone(),
258 remove_vanished,
c06c1b4b 259 max_depth,
062edce2 260 group_filter,
2d5287fb 261 limit,
6e9e6c7a
FG
262 )?;
263 let client = pull_params.client().await?;
de8ec041
DM
264
265 // fixme: set to_stdout to false?
dc7a5b34
TL
266 let upid_str = WorkerTask::spawn(
267 "sync",
268 Some(store.clone()),
269 auth_id.to_string(),
270 true,
271 move |worker| async move {
c06c1b4b
FG
272 task_log!(
273 worker,
274 "pull datastore '{}' from '{}/{}'",
275 store,
276 remote,
277 remote_store,
278 );
de8ec041 279
d9aad37f 280 let pull_future = pull_store(&worker, &client, pull_params);
dc7a5b34
TL
281 let future = select! {
282 success = pull_future.fuse() => success,
283 abort = worker.abort_future().map(|_| Err(format_err!("pull aborted"))) => abort,
284 };
36700a0a 285
dc7a5b34 286 let _ = future?;
de8ec041 287
c06c1b4b 288 task_log!(worker, "pull datastore '{}' end", store);
de8ec041 289
dc7a5b34
TL
290 Ok(())
291 },
292 )?;
de8ec041
DM
293
294 Ok(upid_str)
295}
296
dc7a5b34 297pub const ROUTER: Router = Router::new().post(&API_METHOD_PULL);