]>
Commit | Line | Data |
---|---|---|
cad540e9 WB |
1 | use std::fs::File; |
2 | use std::io::{BufRead, BufReader}; | |
063ca5be | 3 | |
dbd45a72 | 4 | use anyhow::{bail, Error}; |
063ca5be DM |
5 | use serde_json::{json, Value}; |
6 | ||
e7cb4dc5 | 7 | use proxmox::api::{api, Router, RpcEnvironment, Permission}; |
cad540e9 | 8 | use proxmox::api::router::SubdirMap; |
9ea4bce4 | 9 | use proxmox::{identity, list_subdirs_api_method, sortable}; |
552c2259 DM |
10 | |
11 | use crate::tools; | |
dbd45a72 | 12 | |
4ebf0eab | 13 | use crate::api2::types::*; |
dbd45a72 FG |
14 | use crate::api2::pull::check_pull_privs; |
15 | ||
768e10d0 | 16 | use crate::server::{self, UPID, TaskState, TaskListInfoIterator}; |
dbd45a72 FG |
17 | use crate::config::acl::{ |
18 | PRIV_DATASTORE_MODIFY, | |
19 | PRIV_DATASTORE_VERIFY, | |
20 | PRIV_SYS_AUDIT, | |
21 | PRIV_SYS_MODIFY, | |
22 | }; | |
720af9f6 DM |
23 | use crate::config::cached_user_info::CachedUserInfo; |
24 | ||
dbd45a72 FG |
25 | // matches respective job execution privileges |
26 | fn check_job_privs(auth_id: &Authid, user_info: &CachedUserInfo, upid: &UPID) -> Result<(), Error> { | |
27 | match (upid.worker_type.as_str(), &upid.worker_id) { | |
28 | ("verificationjob", Some(workerid)) => { | |
29 | if let Some(captures) = VERIFICATION_JOB_WORKER_ID_REGEX.captures(&workerid) { | |
30 | if let Some(store) = captures.get(1) { | |
31 | return user_info.check_privs(&auth_id, | |
32 | &["datastore", store.as_str()], | |
33 | PRIV_DATASTORE_VERIFY, | |
34 | true); | |
35 | } | |
36 | } | |
37 | }, | |
38 | ("syncjob", Some(workerid)) => { | |
39 | if let Some(captures) = SYNC_JOB_WORKER_ID_REGEX.captures(&workerid) { | |
40 | let remote = captures.get(1); | |
41 | let remote_store = captures.get(2); | |
42 | let local_store = captures.get(3); | |
43 | ||
44 | if let (Some(remote), Some(remote_store), Some(local_store)) = | |
45 | (remote, remote_store, local_store) { | |
46 | ||
47 | return check_pull_privs(&auth_id, | |
48 | local_store.as_str(), | |
49 | remote.as_str(), | |
50 | remote_store.as_str(), | |
51 | false); | |
52 | } | |
53 | } | |
54 | }, | |
55 | ("garbage_collection", Some(workerid)) => { | |
56 | return user_info.check_privs(&auth_id, | |
57 | &["datastore", &workerid], | |
58 | PRIV_DATASTORE_MODIFY, | |
59 | true) | |
60 | }, | |
61 | ("prune", Some(workerid)) => { | |
62 | return user_info.check_privs(&auth_id, | |
63 | &["datastore", | |
64 | &workerid], | |
65 | PRIV_DATASTORE_MODIFY, | |
66 | true); | |
67 | }, | |
68 | _ => bail!("not a scheduled job task"), | |
69 | }; | |
70 | ||
71 | bail!("not a scheduled job task"); | |
72 | } | |
73 | ||
16245d54 FG |
74 | fn check_task_access(auth_id: &Authid, upid: &UPID) -> Result<(), Error> { |
75 | let task_auth_id = &upid.auth_id; | |
76 | if auth_id == task_auth_id | |
77 | || (task_auth_id.is_token() && &Authid::from(task_auth_id.user().clone()) == auth_id) { | |
dbd45a72 | 78 | // task owner can always read |
16245d54 FG |
79 | Ok(()) |
80 | } else { | |
81 | let user_info = CachedUserInfo::new()?; | |
dbd45a72 FG |
82 | |
83 | let task_privs = user_info.lookup_privs(auth_id, &["system", "tasks"]); | |
84 | if task_privs & PRIV_SYS_AUDIT != 0 { | |
85 | // allowed to read all tasks in general | |
86 | Ok(()) | |
87 | } else if check_job_privs(&auth_id, &user_info, upid).is_ok() { | |
88 | // job which the user/token could have configured/manually executed | |
89 | Ok(()) | |
90 | } else { | |
91 | bail!("task access not allowed"); | |
92 | } | |
16245d54 FG |
93 | } |
94 | } | |
5a12c0e2 | 95 | |
83b6a7cf DM |
96 | #[api( |
97 | input: { | |
98 | properties: { | |
99 | node: { | |
100 | schema: NODE_SCHEMA, | |
101 | }, | |
102 | upid: { | |
103 | schema: UPID_SCHEMA, | |
104 | }, | |
105 | }, | |
106 | }, | |
107 | returns: { | |
9809772b | 108 | description: "Task status information.", |
83b6a7cf DM |
109 | properties: { |
110 | node: { | |
111 | schema: NODE_SCHEMA, | |
112 | }, | |
113 | upid: { | |
114 | schema: UPID_SCHEMA, | |
115 | }, | |
116 | pid: { | |
117 | type: i64, | |
118 | description: "The Unix PID.", | |
119 | }, | |
120 | pstart: { | |
121 | type: u64, | |
122 | description: "The Unix process start time from `/proc/pid/stat`", | |
123 | }, | |
124 | starttime: { | |
125 | type: i64, | |
126 | description: "The task start time (Epoch)", | |
127 | }, | |
128 | "type": { | |
129 | type: String, | |
130 | description: "Worker type (arbitrary ASCII string)", | |
131 | }, | |
132 | id: { | |
133 | type: String, | |
134 | optional: true, | |
135 | description: "Worker ID (arbitrary ASCII string)", | |
136 | }, | |
137 | user: { | |
16245d54 | 138 | type: Userid, |
83b6a7cf DM |
139 | description: "The user who started the task.", |
140 | }, | |
16245d54 FG |
141 | tokenid: { |
142 | type: Tokenname, | |
143 | optional: true, | |
144 | }, | |
83b6a7cf DM |
145 | status: { |
146 | type: String, | |
147 | description: "'running' or 'stopped'", | |
148 | }, | |
149 | exitstatus: { | |
150 | type: String, | |
151 | optional: true, | |
152 | description: "'OK', 'Error: <msg>', or 'unkwown'.", | |
153 | }, | |
154 | }, | |
155 | }, | |
156 | access: { | |
9809772b | 157 | description: "Users can access their own tasks, or need Sys.Audit on /system/tasks.", |
720af9f6 | 158 | permission: &Permission::Anybody, |
83b6a7cf DM |
159 | }, |
160 | )] | |
161 | /// Get task status. | |
5751e495 | 162 | async fn get_task_status( |
5a12c0e2 | 163 | param: Value, |
720af9f6 | 164 | rpcenv: &mut dyn RpcEnvironment, |
5a12c0e2 DM |
165 | ) -> Result<Value, Error> { |
166 | ||
167 | let upid = extract_upid(¶m)?; | |
168 | ||
e6dc35ac | 169 | let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?; |
16245d54 | 170 | check_task_access(&auth_id, &upid)?; |
720af9f6 | 171 | |
c360bd73 DM |
172 | let mut result = json!({ |
173 | "upid": param["upid"], | |
174 | "node": upid.node, | |
175 | "pid": upid.pid, | |
176 | "pstart": upid.pstart, | |
177 | "starttime": upid.starttime, | |
178 | "type": upid.worker_type, | |
179 | "id": upid.worker_id, | |
16245d54 | 180 | "user": upid.auth_id.user(), |
c360bd73 DM |
181 | }); |
182 | ||
16245d54 FG |
183 | if upid.auth_id.is_token() { |
184 | result["tokenid"] = Value::from(upid.auth_id.tokenname().unwrap().as_str()); | |
185 | } | |
186 | ||
5751e495 | 187 | if crate::server::worker_is_active(&upid).await? { |
c360bd73 | 188 | result["status"] = Value::from("running"); |
5a12c0e2 | 189 | } else { |
77bd2a46 | 190 | let exitstatus = crate::server::upid_read_status(&upid).unwrap_or(TaskState::Unknown { endtime: 0 }); |
c360bd73 | 191 | result["status"] = Value::from("stopped"); |
4c116baf | 192 | result["exitstatus"] = Value::from(exitstatus.to_string()); |
5a12c0e2 DM |
193 | }; |
194 | ||
195 | Ok(result) | |
196 | } | |
197 | ||
198 | fn extract_upid(param: &Value) -> Result<UPID, Error> { | |
199 | ||
200 | let upid_str = tools::required_string_param(¶m, "upid")?; | |
201 | ||
8560fe3e | 202 | upid_str.parse::<UPID>() |
5a12c0e2 DM |
203 | } |
204 | ||
83b6a7cf DM |
205 | #[api( |
206 | input: { | |
207 | properties: { | |
208 | node: { | |
209 | schema: NODE_SCHEMA, | |
210 | }, | |
211 | upid: { | |
212 | schema: UPID_SCHEMA, | |
213 | }, | |
214 | "test-status": { | |
215 | type: bool, | |
216 | optional: true, | |
217 | description: "Test task status, and set result attribute \"active\" accordingly.", | |
218 | }, | |
219 | start: { | |
220 | type: u64, | |
221 | optional: true, | |
222 | description: "Start at this line.", | |
223 | default: 0, | |
224 | }, | |
225 | limit: { | |
226 | type: u64, | |
227 | optional: true, | |
228 | description: "Only list this amount of lines.", | |
229 | default: 50, | |
230 | }, | |
231 | }, | |
232 | }, | |
233 | access: { | |
720af9f6 DM |
234 | description: "Users can access there own tasks, or need Sys.Audit on /system/tasks.", |
235 | permission: &Permission::Anybody, | |
83b6a7cf DM |
236 | }, |
237 | )] | |
238 | /// Read task log. | |
5751e495 | 239 | async fn read_task_log( |
5a12c0e2 | 240 | param: Value, |
e8d1da6a | 241 | mut rpcenv: &mut dyn RpcEnvironment, |
5a12c0e2 DM |
242 | ) -> Result<Value, Error> { |
243 | ||
244 | let upid = extract_upid(¶m)?; | |
6b508dd5 | 245 | |
e6dc35ac | 246 | let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?; |
720af9f6 | 247 | |
16245d54 | 248 | check_task_access(&auth_id, &upid)?; |
720af9f6 | 249 | |
6b508dd5 DM |
250 | let test_status = param["test-status"].as_bool().unwrap_or(false); |
251 | ||
5a12c0e2 DM |
252 | let start = param["start"].as_u64().unwrap_or(0); |
253 | let mut limit = param["limit"].as_u64().unwrap_or(50); | |
6b508dd5 | 254 | |
5a12c0e2 DM |
255 | let mut count: u64 = 0; |
256 | ||
257 | let path = upid.log_path(); | |
258 | ||
259 | let file = File::open(path)?; | |
260 | ||
261 | let mut lines: Vec<Value> = vec![]; | |
262 | ||
263 | for line in BufReader::new(file).lines() { | |
264 | match line { | |
265 | Ok(line) => { | |
266 | count += 1; | |
267 | if count < start { continue }; | |
11377a47 | 268 | if limit == 0 { continue }; |
5a12c0e2 DM |
269 | |
270 | lines.push(json!({ "n": count, "t": line })); | |
271 | ||
272 | limit -= 1; | |
273 | } | |
274 | Err(err) => { | |
275 | log::error!("reading task log failed: {}", err); | |
276 | break; | |
277 | } | |
278 | } | |
279 | } | |
280 | ||
e8d1da6a | 281 | rpcenv["total"] = Value::from(count); |
d8d40dd0 | 282 | |
6b508dd5 | 283 | if test_status { |
5751e495 | 284 | let active = crate::server::worker_is_active(&upid).await?; |
e8d1da6a | 285 | rpcenv["active"] = Value::from(active); |
6b508dd5 DM |
286 | } |
287 | ||
5a12c0e2 DM |
288 | Ok(json!(lines)) |
289 | } | |
063ca5be | 290 | |
83b6a7cf DM |
291 | #[api( |
292 | protected: true, | |
293 | input: { | |
294 | properties: { | |
295 | node: { | |
296 | schema: NODE_SCHEMA, | |
297 | }, | |
298 | upid: { | |
299 | schema: UPID_SCHEMA, | |
300 | }, | |
301 | }, | |
302 | }, | |
303 | access: { | |
720af9f6 DM |
304 | description: "Users can stop there own tasks, or need Sys.Modify on /system/tasks.", |
305 | permission: &Permission::Anybody, | |
83b6a7cf DM |
306 | }, |
307 | )] | |
308 | /// Try to stop a task. | |
a665dea1 DM |
309 | fn stop_task( |
310 | param: Value, | |
720af9f6 | 311 | rpcenv: &mut dyn RpcEnvironment, |
a665dea1 DM |
312 | ) -> Result<Value, Error> { |
313 | ||
314 | let upid = extract_upid(¶m)?; | |
315 | ||
e6dc35ac | 316 | let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?; |
720af9f6 | 317 | |
e6dc35ac | 318 | if auth_id != upid.auth_id { |
720af9f6 | 319 | let user_info = CachedUserInfo::new()?; |
e6dc35ac | 320 | user_info.check_privs(&auth_id, &["system", "tasks"], PRIV_SYS_MODIFY, false)?; |
720af9f6 DM |
321 | } |
322 | ||
5751e495 | 323 | server::abort_worker_async(upid); |
a665dea1 DM |
324 | |
325 | Ok(Value::Null) | |
326 | } | |
327 | ||
2c4b303c DM |
328 | #[api( |
329 | input: { | |
330 | properties: { | |
331 | node: { | |
332 | schema: NODE_SCHEMA | |
333 | }, | |
334 | start: { | |
335 | type: u64, | |
336 | description: "List tasks beginning from this offset.", | |
337 | default: 0, | |
338 | optional: true, | |
339 | }, | |
340 | limit: { | |
341 | type: u64, | |
e7dd169f | 342 | description: "Only list this amount of tasks. (0 means no limit)", |
2c4b303c DM |
343 | default: 50, |
344 | optional: true, | |
345 | }, | |
346 | store: { | |
347 | schema: DATASTORE_SCHEMA, | |
348 | optional: true, | |
349 | }, | |
350 | running: { | |
351 | type: bool, | |
352 | description: "Only list running tasks.", | |
353 | optional: true, | |
ca9dfe5f | 354 | default: false, |
2c4b303c DM |
355 | }, |
356 | errors: { | |
357 | type: bool, | |
358 | description: "Only list erroneous tasks.", | |
359 | optional:true, | |
ca9dfe5f | 360 | default: false, |
2c4b303c DM |
361 | }, |
362 | userfilter: { | |
e7cb4dc5 | 363 | optional: true, |
2c4b303c DM |
364 | type: String, |
365 | description: "Only list tasks from this user.", | |
366 | }, | |
a2a7dd15 DC |
367 | since: { |
368 | type: i64, | |
369 | description: "Only list tasks since this UNIX epoch.", | |
370 | optional: true, | |
371 | }, | |
c1fa057c DC |
372 | until: { |
373 | type: i64, | |
374 | description: "Only list tasks until this UNIX epoch.", | |
375 | optional: true, | |
376 | }, | |
a2a7dd15 DC |
377 | typefilter: { |
378 | optional: true, | |
379 | type: String, | |
380 | description: "Only list tasks whose type contains this.", | |
381 | }, | |
382 | statusfilter: { | |
383 | optional: true, | |
384 | type: Array, | |
385 | description: "Only list tasks which have any one of the listed status.", | |
386 | items: { | |
387 | type: TaskStateType, | |
388 | }, | |
389 | }, | |
2c4b303c DM |
390 | }, |
391 | }, | |
392 | returns: { | |
393 | description: "A list of tasks.", | |
394 | type: Array, | |
99384f79 | 395 | items: { type: TaskListItem }, |
2c4b303c | 396 | }, |
83b6a7cf | 397 | access: { |
720af9f6 DM |
398 | description: "Users can only see there own tasks, unless the have Sys.Audit on /system/tasks.", |
399 | permission: &Permission::Anybody, | |
83b6a7cf | 400 | }, |
2c4b303c DM |
401 | )] |
402 | /// List tasks. | |
8528fce8 | 403 | pub fn list_tasks( |
ca9dfe5f DM |
404 | start: u64, |
405 | limit: u64, | |
406 | errors: bool, | |
407 | running: bool, | |
005a5b96 | 408 | userfilter: Option<String>, |
a2a7dd15 | 409 | since: Option<i64>, |
c1fa057c | 410 | until: Option<i64>, |
a2a7dd15 DC |
411 | typefilter: Option<String>, |
412 | statusfilter: Option<Vec<TaskStateType>>, | |
063ca5be | 413 | param: Value, |
e8d1da6a | 414 | mut rpcenv: &mut dyn RpcEnvironment, |
99384f79 | 415 | ) -> Result<Vec<TaskListItem>, Error> { |
063ca5be | 416 | |
e6dc35ac | 417 | let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?; |
720af9f6 | 418 | let user_info = CachedUserInfo::new()?; |
e6dc35ac | 419 | let user_privs = user_info.lookup_privs(&auth_id, &["system", "tasks"]); |
720af9f6 DM |
420 | |
421 | let list_all = (user_privs & PRIV_SYS_AUDIT) != 0; | |
422 | ||
567d3e00 DM |
423 | let store = param["store"].as_str(); |
424 | ||
768e10d0 | 425 | let list = TaskListInfoIterator::new(running)?; |
e7dd169f | 426 | let limit = if limit > 0 { limit as usize } else { usize::MAX }; |
d2a2e02b | 427 | |
768e10d0 | 428 | let result: Vec<TaskListItem> = list |
c1fa057c DC |
429 | .skip_while(|info| { |
430 | match (info, until) { | |
431 | (Ok(info), Some(until)) => info.upid.starttime > until, | |
432 | (Ok(_), None) => false, | |
433 | (Err(_), _) => false, | |
434 | } | |
435 | }) | |
a2a7dd15 DC |
436 | .take_while(|info| { |
437 | match (info, since) { | |
438 | (Ok(info), Some(since)) => info.upid.starttime > since, | |
439 | (Ok(_), None) => true, | |
440 | (Err(_), _) => false, | |
441 | } | |
442 | }) | |
768e10d0 DC |
443 | .filter_map(|info| { |
444 | let info = match info { | |
445 | Ok(info) => info, | |
446 | Err(_) => return None, | |
447 | }; | |
720af9f6 | 448 | |
16245d54 FG |
449 | if !list_all && check_task_access(&auth_id, &info.upid).is_err() { |
450 | return None; | |
451 | } | |
063ca5be | 452 | |
e6dc35ac FG |
453 | if let Some(needle) = &userfilter { |
454 | if !info.upid.auth_id.to_string().contains(needle) { return None; } | |
d2a2e02b DM |
455 | } |
456 | ||
567d3e00 DM |
457 | if let Some(store) = store { |
458 | // Note: useful to select all tasks spawned by proxmox-backup-client | |
459 | let worker_id = match &info.upid.worker_id { | |
460 | Some(w) => w, | |
768e10d0 | 461 | None => return None, // skip |
567d3e00 DM |
462 | }; |
463 | ||
503995c7 DM |
464 | if info.upid.worker_type == "backup" || info.upid.worker_type == "restore" || |
465 | info.upid.worker_type == "prune" | |
466 | { | |
4ebda996 | 467 | let prefix = format!("{}:", store); |
768e10d0 | 468 | if !worker_id.starts_with(&prefix) { return None; } |
503995c7 | 469 | } else if info.upid.worker_type == "garbage_collection" { |
768e10d0 | 470 | if worker_id != store { return None; } |
567d3e00 | 471 | } else { |
768e10d0 | 472 | return None; // skip |
567d3e00 DM |
473 | } |
474 | } | |
475 | ||
a2a7dd15 DC |
476 | if let Some(typefilter) = &typefilter { |
477 | if !info.upid.worker_type.contains(typefilter) { | |
478 | return None; | |
479 | } | |
480 | } | |
481 | ||
482 | match (&info.state, &statusfilter) { | |
483 | (Some(_), _) if running => return None, | |
484 | (Some(crate::server::TaskState::OK { .. }), _) if errors => return None, | |
485 | (Some(state), Some(filters)) => { | |
486 | if !filters.contains(&state.tasktype()) { | |
487 | return None; | |
488 | } | |
489 | }, | |
490 | (None, Some(_)) => return None, | |
768e10d0 | 491 | _ => {}, |
063ca5be DM |
492 | } |
493 | ||
768e10d0 DC |
494 | Some(info.into()) |
495 | }).skip(start as usize) | |
e7dd169f | 496 | .take(limit) |
768e10d0 | 497 | .collect(); |
063ca5be | 498 | |
768e10d0 | 499 | let mut count = result.len() + start as usize; |
e7dd169f | 500 | if result.len() > 0 && result.len() >= limit { // we have a 'virtual' entry as long as we have any new |
768e10d0 | 501 | count += 1; |
063ca5be DM |
502 | } |
503 | ||
e8d1da6a | 504 | rpcenv["total"] = Value::from(count); |
063ca5be | 505 | |
0196b9bf | 506 | Ok(result) |
063ca5be DM |
507 | } |
508 | ||
552c2259 | 509 | #[sortable] |
83b6a7cf | 510 | const UPID_API_SUBDIRS: SubdirMap = &sorted!([ |
255f378a DM |
511 | ( |
512 | "log", &Router::new() | |
83b6a7cf | 513 | .get(&API_METHOD_READ_TASK_LOG) |
255f378a DM |
514 | ), |
515 | ( | |
516 | "status", &Router::new() | |
83b6a7cf | 517 | .get(&API_METHOD_GET_TASK_STATUS) |
255f378a | 518 | ) |
83b6a7cf | 519 | ]); |
255f378a DM |
520 | |
521 | pub const UPID_API_ROUTER: Router = Router::new() | |
522 | .get(&list_subdirs_api_method!(UPID_API_SUBDIRS)) | |
83b6a7cf | 523 | .delete(&API_METHOD_STOP_TASK) |
255f378a DM |
524 | .subdirs(&UPID_API_SUBDIRS); |
525 | ||
526 | pub const ROUTER: Router = Router::new() | |
2c4b303c | 527 | .get(&API_METHOD_LIST_TASKS) |
255f378a | 528 | .match_all("upid", &UPID_API_ROUTER); |