]> git.proxmox.com Git - proxmox-backup.git/blame - src/api2/node/tasks.rs
ui: recommit onlinehelp
[proxmox-backup.git] / src / api2 / node / tasks.rs
CommitLineData
cad540e9
WB
1use std::fs::File;
2use std::io::{BufRead, BufReader};
063ca5be 3
dbd45a72 4use anyhow::{bail, Error};
063ca5be
DM
5use serde_json::{json, Value};
6
e7cb4dc5 7use proxmox::api::{api, Router, RpcEnvironment, Permission};
cad540e9 8use proxmox::api::router::SubdirMap;
9ea4bce4 9use proxmox::{identity, list_subdirs_api_method, sortable};
552c2259
DM
10
11use crate::tools;
dbd45a72 12
4ebf0eab 13use crate::api2::types::*;
dbd45a72
FG
14use crate::api2::pull::check_pull_privs;
15
768e10d0 16use crate::server::{self, UPID, TaskState, TaskListInfoIterator};
dbd45a72
FG
17use crate::config::acl::{
18 PRIV_DATASTORE_MODIFY,
19 PRIV_DATASTORE_VERIFY,
20 PRIV_SYS_AUDIT,
21 PRIV_SYS_MODIFY,
22};
720af9f6
DM
23use crate::config::cached_user_info::CachedUserInfo;
24
dbd45a72
FG
25// matches respective job execution privileges
26fn 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
74fn 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 162async 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(&param)?;
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
198fn extract_upid(param: &Value) -> Result<UPID, Error> {
199
200 let upid_str = tools::required_string_param(&param, "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 239async 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(&param)?;
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
309fn stop_task(
310 param: Value,
720af9f6 311 rpcenv: &mut dyn RpcEnvironment,
a665dea1
DM
312) -> Result<Value, Error> {
313
314 let upid = extract_upid(&param)?;
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 403pub 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 510const 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
521pub 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
526pub const ROUTER: Router = Router::new()
2c4b303c 527 .get(&API_METHOD_LIST_TASKS)
255f378a 528 .match_all("upid", &UPID_API_ROUTER);